Merge pull request #1439 from affaan-m/fix/urgent-install-and-name

fix: unblock urgent install and gateguard regressions
This commit is contained in:
Affaan Mustafa
2026-04-14 20:36:06 -07:00
committed by GitHub
19 changed files with 542 additions and 93 deletions

View File

@@ -174,9 +174,11 @@ 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
``` ```
> Install-name clarification: older posts may still show `ecc@ecc`. That shorthand is deprecated. Anthropic marketplace/plugin installs are keyed by a canonical plugin identifier, so ECC standardized on `everything-claude-code@everything-claude-code` to keep the listing name, install path, `/plugin list`, and repo docs aligned instead of maintaining two different public names for the same plugin.
### Step 2: Install Rules (Required) ### Step 2: Install Rules (Required)
> WARNING: **Important:** Claude Code plugins cannot distribute `rules` automatically. Install them manually: > WARNING: **Important:** Claude Code plugins cannot distribute `rules` automatically. Install them manually:
@@ -236,7 +238,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 48 agents, 183 skills, and 79 legacy command shims. **That's it!** You now have access to 48 agents, 183 skills, and 79 legacy command shims.
@@ -648,7 +650,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 +666,7 @@ Or add directly to your `~/.claude/settings.json`:
} }
}, },
"enabledPlugins": { "enabledPlugins": {
"ecc@ecc": true "everything-claude-code@everything-claude-code": true
} }
} }
``` ```
@@ -882,7 +884,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.

View File

@@ -99,12 +99,14 @@
```bash ```bash
# 添加市场 # 添加市场
/plugin marketplace add 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
``` ```
> 安装名称说明:较早的帖子里可能还会出现 `ecc@ecc`。那个旧缩写现在已经废弃。Anthropic 的 marketplace/plugin 安装是按规范化插件标识符寻址的,因此 ECC 统一为 `everything-claude-code@everything-claude-code`,这样市场条目、安装命令、`/plugin list` 输出和仓库文档都使用同一个公开名称,不再出现两个名字指向同一插件的混乱。
### 第二步:安装规则(必需) ### 第二步:安装规则(必需)
> WARNING: **重要提示:** Claude Code 插件无法自动分发 `rules`,需要手动安装: > WARNING: **重要提示:** Claude Code 插件无法自动分发 `rules`,需要手动安装:
@@ -159,7 +161,7 @@ npx ecc-install typescript
# /plan "添加用户认证" # /plan "添加用户认证"
# 查看可用命令 # 查看可用命令
/plugin list ecc@ecc /plugin list everything-claude-code@everything-claude-code
``` ```
**完成!** 你现在可以使用 48 个代理、183 个技能和 79 个命令。 **完成!** 你现在可以使用 48 个代理、183 个技能和 79 个命令。
@@ -543,10 +545,10 @@ Claude Code v2.1+ 会**按照约定自动加载**已安装插件中的 `hooks/ho
```bash ```bash
# 将此仓库添加为市场 # 将此仓库添加为市场
/plugin marketplace add 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`
@@ -562,7 +564,7 @@ Claude Code v2.1+ 会**按照约定自动加载**已安装插件中的 `hooks/ho
} }
}, },
"enabledPlugins": { "enabledPlugins": {
"ecc@ecc": true "everything-claude-code@everything-claude-code": true
} }
} }
``` ```

View File

@@ -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
} }
} }
``` ```

View File

@@ -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" と言って起動します
--- ---

View File

@@ -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
``` ```
플러그인에서 사용할 수 있는 모든 에이전트, 커맨드, 스킬을 보여줍니다. 플러그인에서 사용할 수 있는 모든 에이전트, 커맨드, 스킬을 보여줍니다.

View File

@@ -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>

View File

@@ -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.

View File

@@ -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
``` ```
**搞定!** 你现在可以使用 48 个智能体、183 项技能和 79 个命令了。 **搞定!** 你现在可以使用 48 个智能体、183 项技能和 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
``` ```
这会显示插件中所有可用的代理、命令和技能。 这会显示插件中所有可用的代理、命令和技能。

View File

@@ -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" 激活
*** ***

View File

@@ -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
} }
} }
``` ```

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
const fs = require('fs'); const fs = require('fs');
const os = require('os');
const path = require('path'); const path = require('path');
const CATEGORIES = [ const CATEGORIES = [
@@ -187,7 +188,7 @@ function detectTargetMode(rootDir) {
} }
function findPluginInstall(rootDir) { function findPluginInstall(rootDir) {
const homeDir = process.env.HOME || ''; const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir() || '';
const pluginDirs = [ const pluginDirs = [
'ecc', 'ecc',
'ecc@ecc', 'ecc@ecc',

View File

@@ -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 hashSessionKey('sid', raw);
}
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);
saveState(state); if (found && Date.now() - (state.last_active || 0) > READ_HEARTBEAT_MS) {
saveState(state);
}
return found; return found;
} }
@@ -109,9 +177,13 @@ function isChecked(key) {
for (const f of files) { for (const f of files) {
if (!f.startsWith('state-') || !f.endsWith('.json')) continue; if (!f.startsWith('state-') || !f.endsWith('.json')) continue;
const fp = path.join(STATE_DIR, f); const fp = path.join(STATE_DIR, f);
const stat = fs.statSync(fp); try {
if (now - stat.mtimeMs > SESSION_TIMEOUT_MS * 2) { const stat = fs.statSync(fp);
fs.unlinkSync(fp); if (now - stat.mtimeMs > SESSION_TIMEOUT_MS * 2) {
fs.unlinkSync(fp);
}
} catch (_) {
// Ignore files that disappear between readdir/stat/unlink.
} }
} }
} catch (_) { /* ignore */ } } catch (_) { /* ignore */ }
@@ -125,15 +197,62 @@ function sanitizePath(filePath) {
for (const char of String(filePath || '')) { for (const char of String(filePath || '')) {
const code = char.codePointAt(0); const code = char.codePointAt(0);
const isAsciiControl = code <= 0x1f || code === 0x7f; const isAsciiControl = code <= 0x1f || code === 0x7f;
const isBidiOverride = const isBidiOverride = (code >= 0x200e && code <= 0x200f) || (code >= 0x202a && code <= 0x202e) || (code >= 0x2066 && code <= 0x2069);
(code >= 0x200e && code <= 0x200f) || sanitized += (isAsciiControl || isBidiOverride) ? ' ' : char;
(code >= 0x202a && code <= 0x202e) ||
(code >= 0x2066 && code <= 0x2069);
sanitized += isAsciiControl || isBidiOverride ? ' ' : char;
} }
return sanitized.trim().slice(0, 500); return sanitized.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 || /[\r\n;&|><`$()]/.test(trimmed)) {
return false;
}
const tokens = trimmed.split(/\s+/);
if (tokens[0] !== 'git' || tokens.length < 2) {
return false;
}
const subcommand = tokens[1].toLowerCase();
const args = tokens.slice(2);
if (subcommand === 'status') {
return args.every(arg => ['--porcelain', '--short', '--branch'].includes(arg));
}
if (subcommand === 'diff') {
return args.length <= 1 && args.every(arg => ['--name-only', '--name-status'].includes(arg));
}
if (subcommand === 'log') {
return args.every(arg => arg === '--oneline' || /^--max-count=\d+$/.test(arg));
}
if (subcommand === 'show') {
return args.length === 1 && !args[0].startsWith('--') && /^[a-zA-Z0-9._:/-]+$/.test(args[0]);
}
if (subcommand === 'branch') {
return args.length === 1 && args[0] === '--show-current';
}
if (subcommand === 'rev-parse') {
return args.length === 2 && args[0] === '--abbrev-ref' && /^head$/i.test(args[1]);
}
return false;
}
// --- Gate messages --- // --- Gate messages ---
function editGateMsg(filePath) { function editGateMsg(filePath) {
@@ -215,6 +334,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 || {};
@@ -224,7 +345,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
} }
@@ -240,7 +361,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));
} }
@@ -250,6 +371,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

View File

@@ -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;
} }

View File

@@ -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"
--- ---

View File

@@ -10,10 +10,12 @@ const { spawnSync } = require('child_process');
const runner = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'run-with-flags.js'); const runner = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'run-with-flags.js');
const externalStateDir = process.env.GATEGUARD_STATE_DIR; const externalStateDir = process.env.GATEGUARD_STATE_DIR;
const tmpRoot = process.env.TMPDIR || process.env.TEMP || process.env.TMP || '/tmp'; const tmpRoot = process.env.TMPDIR || process.env.TEMP || process.env.TMP || '/tmp';
const stateDir = externalStateDir || fs.mkdtempSync(path.join(tmpRoot, 'gateguard-test-')); const baseStateDir = externalStateDir || tmpRoot;
const stateDir = fs.mkdtempSync(path.join(baseStateDir, 'gateguard-test-'));
// 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 +31,12 @@ function test(name, fn) {
function clearState() { function clearState() {
try { try {
if (fs.existsSync(stateFile)) { if (fs.existsSync(stateDir)) {
fs.unlinkSync(stateFile); fs.rmSync(stateDir, { recursive: true, force: true });
} }
fs.mkdirSync(stateDir, { recursive: true });
} 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 +366,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 +417,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,15 +449,126 @@ 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++;
// Cleanup only the temp directory created by this test file. // --- Test 15: raw input session IDs provide stable retry state without env vars ---
if (!externalStateDir) { clearState();
try { if (test('uses raw input session_id when hook env vars are missing', () => {
if (fs.existsSync(stateDir)) { const input = {
fs.rmSync(stateDir, { recursive: true, force: true }); session_id: 'raw-session-1234',
} tool_name: 'Bash',
} catch (err) { tool_input: { command: 'ls -la' }
console.error(` [cleanup] failed to remove ${stateDir}: ${err.message}`); };
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++;
// --- Test 18: rejects mutating git commands that only share a prefix ---
clearState();
if (test('does not treat mutating git commands as read-only introspection', () => {
const input = {
tool_name: 'Bash',
tool_input: { command: 'git status && rm -rf /tmp/demo' }
};
const result = runBashHook(input);
const output = parseOutput(result.stdout);
assert.ok(output, 'should produce valid JSON output');
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny');
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('current instruction'));
})) passed++; else failed++;
// --- Test 19: long raw session IDs hash instead of collapsing to project fallback ---
clearState();
if (test('uses a stable hash for long raw session ids', () => {
const longSessionId = `session-${'x'.repeat(120)}`;
const input = {
session_id: longSessionId,
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 stateFiles = fs.readdirSync(stateDir).filter(entry => entry.startsWith('state-') && entry.endsWith('.json'));
assert.strictEqual(stateFiles.length, 1, 'long raw session id should still produce a dedicated state file');
assert.ok(/state-sid-[a-f0-9]{24}\.json$/.test(stateFiles[0]), 'long raw session ids should hash to a bounded sid-* key');
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 long raw session_id is stable');
} else {
assert.strictEqual(secondOutput.tool_name, 'Bash');
}
})) passed++; else failed++;
// Cleanup only the temp directory created by this test file.
try {
if (fs.existsSync(stateDir)) {
fs.rmSync(stateDir, { recursive: true, force: true });
}
} catch (err) {
console.error(` [cleanup] failed to remove ${stateDir}: ${err.message}`);
} }
console.log(`\n ${passed} passed, ${failed} failed\n`); console.log(`\n ${passed} passed, ${failed} failed\n`);

View File

@@ -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++;

View File

@@ -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++;

View File

@@ -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,51 @@ test('README version row matches package.json', () => {
assert.strictEqual(match[1], expectedVersion); assert.strictEqual(match[1], expectedVersion);
}); });
test('user-facing docs do not use deprecated ecc@ecc install commands', () => {
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 (/\/plugin\s+(install|list)\s+ecc@ecc\b/.test(source)) {
offenders.push(path.relative(repoRoot, filePath));
}
}
assert.deepStrictEqual(
offenders,
[],
`Deprecated ecc@ecc install commands must not appear in user-facing docs: ${offenders.join(', ')}`,
);
});
test('user-facing docs do not use the legacy non-URL marketplace add form', () => {
const markdownFiles = [
path.join(repoRoot, 'README.md'),
path.join(repoRoot, 'README.zh-CN.md'),
...collectMarkdownFiles(path.join(repoRoot, 'docs')),
];
const offenders = [];
for (const filePath of markdownFiles) {
const source = fs.readFileSync(filePath, 'utf8');
if (source.includes('/plugin marketplace add affaan-m/everything-claude-code')) {
offenders.push(path.relative(repoRoot, filePath));
}
}
assert.deepStrictEqual(
offenders,
[],
`Legacy non-URL marketplace add form 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);

View File

@@ -19,12 +19,21 @@ function cleanup(dirPath) {
} }
function run(args = [], options = {}) { function run(args = [], options = {}) {
const userProfile = options.userProfile || options.homeDir || process.env.USERPROFILE;
const env = {
...process.env,
USERPROFILE: userProfile,
};
if (Object.prototype.hasOwnProperty.call(options, 'homeDir')) {
env.HOME = options.homeDir;
} else {
env.HOME = process.env.HOME;
}
const stdout = execFileSync('node', [SCRIPT, ...args], { const stdout = execFileSync('node', [SCRIPT, ...args], {
cwd: options.cwd || path.join(__dirname, '..', '..'), cwd: options.cwd || path.join(__dirname, '..', '..'),
env: { env,
...process.env,
HOME: options.homeDir || process.env.HOME,
},
encoding: 'utf8', encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
timeout: 10000, timeout: 10000,
@@ -132,6 +141,109 @@ function runTests() {
} }
})) passed++; else failed++; })) passed++; else failed++;
if (test('detects marketplace-installed Claude plugins under home 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++;
if (test('detects marketplace-installed Claude plugins under project marketplaces/', () => {
const homeDir = createTempDir('harness-audit-marketplace-home-');
const projectRoot = createTempDir('harness-audit-marketplace-project-');
try {
fs.mkdirSync(path.join(projectRoot, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin'), { recursive: true });
fs.writeFileSync(
path.join(projectRoot, '.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++;
if (test('detects marketplace-installed Claude plugins from USERPROFILE fallback on Windows-style setups', () => {
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: '',
userProfile: 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);
} }