mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-15 14:33:33 +08:00
Merge pull request #1439 from affaan-m/fix/urgent-install-and-name
fix: unblock urgent install and gateguard regressions
This commit is contained in:
12
README.md
12
README.md
@@ -174,9 +174,11 @@ Get up and running in under 2 minutes:
|
||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||
|
||||
# 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)
|
||||
|
||||
> 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"
|
||||
|
||||
# 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.
|
||||
@@ -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
|
||||
|
||||
# Install the plugin
|
||||
/plugin install ecc@ecc
|
||||
/plugin install everything-claude-code
|
||||
```
|
||||
|
||||
Or add directly to your `~/.claude/settings.json`:
|
||||
@@ -664,7 +666,7 @@ Or add directly to your `~/.claude/settings.json`:
|
||||
}
|
||||
},
|
||||
"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>
|
||||
|
||||
```bash
|
||||
/plugin list ecc@ecc
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
This shows all available agents, commands, and skills from the plugin.
|
||||
|
||||
@@ -99,12 +99,14 @@
|
||||
|
||||
```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`,需要手动安装:
|
||||
@@ -159,7 +161,7 @@ npx ecc-install typescript
|
||||
# /plan "添加用户认证"
|
||||
|
||||
# 查看可用命令
|
||||
/plugin list ecc@ecc
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
**完成!** 你现在可以使用 48 个代理、183 个技能和 79 个命令。
|
||||
@@ -543,10 +545,10 @@ Claude Code v2.1+ 会**按照约定自动加载**已安装插件中的 `hooks/ho
|
||||
|
||||
```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`:
|
||||
@@ -562,7 +564,7 @@ Claude Code v2.1+ 会**按照约定自动加载**已安装插件中的 `hooks/ho
|
||||
}
|
||||
},
|
||||
"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 install ecc@ecc
|
||||
/plugin install everything-claude-code
|
||||
```
|
||||
|
||||
### ステップ2:ルールをインストール(必須)
|
||||
@@ -140,7 +140,7 @@ cp -r everything-claude-code/rules/golang/* ~/.claude/rules/
|
||||
# /plan "ユーザー認証を追加"
|
||||
|
||||
# 利用可能なコマンドを確認
|
||||
/plugin list ecc@ecc
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
**完了です!** これで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 install ecc@ecc
|
||||
/plugin install everything-claude-code
|
||||
```
|
||||
|
||||
または、`~/.claude/settings.json` に直接追加:
|
||||
@@ -443,7 +443,7 @@ Duplicate hook file detected: ./hooks/hooks.json is already resolved to a loaded
|
||||
}
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"ecc@ecc": true
|
||||
"everything-claude-code@everything-claude-code": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -17,7 +17,7 @@ Everything Claude Code プロジェクトのインタラクティブなステッ
|
||||
## 前提条件
|
||||
|
||||
このスキルは起動前に Claude Code からアクセス可能である必要があります。ブートストラップには2つの方法があります:
|
||||
1. **プラグイン経由**: `/plugin install ecc@ecc` — プラグインがこのスキルを自動的にロードします
|
||||
1. **プラグイン経由**: `/plugin install everything-claude-code` — プラグインがこのスキルを自動的にロードします
|
||||
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 install ecc@ecc
|
||||
/plugin install everything-claude-code
|
||||
```
|
||||
|
||||
### 2단계: 룰 설치 (필수)
|
||||
@@ -147,7 +147,7 @@ cd everything-claude-code
|
||||
# /plan "사용자 인증 추가"
|
||||
|
||||
# 사용 가능한 커맨드 확인
|
||||
/plugin list ecc@ecc
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
**끝!** 이제 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 install ecc@ecc
|
||||
/plugin install everything-claude-code
|
||||
```
|
||||
|
||||
또는 `~/.claude/settings.json`에 직접 추가:
|
||||
@@ -375,7 +375,7 @@ Claude Code v2.1+는 설치된 플러그인의 `hooks/hooks.json`을 **자동으
|
||||
}
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"ecc@ecc": true
|
||||
"everything-claude-code@everything-claude-code": true
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -535,7 +535,7 @@ rules/
|
||||
<summary><b>설치된 에이전트/커맨드 확인은 어떻게 하나요?</b></summary>
|
||||
|
||||
```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
|
||||
|
||||
# Instalar plugin
|
||||
/plugin install ecc@ecc
|
||||
/plugin install everything-claude-code
|
||||
```
|
||||
|
||||
### Passo 2: Instalar as Regras (Obrigatório)
|
||||
@@ -167,7 +167,7 @@ npx ecc-install typescript
|
||||
# /plan "Adicionar autenticação de usuário"
|
||||
|
||||
# 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.
|
||||
@@ -313,7 +313,7 @@ claude --version
|
||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||
|
||||
# Instalar o plugin
|
||||
/plugin install ecc@ecc
|
||||
/plugin install everything-claude-code
|
||||
```
|
||||
|
||||
Ou adicione diretamente ao seu `~/.claude/settings.json`:
|
||||
@@ -329,7 +329,7 @@ Ou adicione diretamente ao seu `~/.claude/settings.json`:
|
||||
}
|
||||
},
|
||||
"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>
|
||||
|
||||
```bash
|
||||
/plugin list ecc@ecc
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
</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'i kur
|
||||
/plugin install ecc@ecc
|
||||
/plugin install everything-claude-code
|
||||
```
|
||||
|
||||
### 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"
|
||||
|
||||
# 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.
|
||||
@@ -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>
|
||||
|
||||
```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.
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||
|
||||
# Install plugin
|
||||
/plugin install ecc@ecc
|
||||
/plugin install everything-claude-code
|
||||
```
|
||||
|
||||
### 步骤 2:安装规则(必需)
|
||||
@@ -206,7 +206,7 @@ npx ecc-install typescript
|
||||
# /plan "Add user authentication"
|
||||
|
||||
# Check available commands
|
||||
/plugin list ecc@ecc
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
**搞定!** 你现在可以使用 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
|
||||
|
||||
# Install the plugin
|
||||
/plugin install ecc@ecc
|
||||
/plugin install everything-claude-code
|
||||
```
|
||||
|
||||
或者直接添加到您的 `~/.claude/settings.json`:
|
||||
@@ -601,7 +601,7 @@ Claude Code v2.1+ **会自动加载** 任何已安装插件中的 `hooks/hooks.j
|
||||
}
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"ecc@ecc": true
|
||||
"everything-claude-code@everything-claude-code": true
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -793,7 +793,7 @@ rules/
|
||||
<summary><b>如何检查已安装的代理/命令?</b></summary>
|
||||
|
||||
```bash
|
||||
/plugin list ecc@ecc
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
这会显示插件中所有可用的代理、命令和技能。
|
||||
|
||||
@@ -19,7 +19,7 @@ origin: ECC
|
||||
|
||||
此技能必须在激活前对 Claude Code 可访问。有两种引导方式:
|
||||
|
||||
1. **通过插件**: `/plugin install ecc@ecc` — 插件会自动加载此技能
|
||||
1. **通过插件**: `/plugin install everything-claude-code` — 插件会自动加载此技能
|
||||
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 install ecc@ecc
|
||||
/plugin install everything-claude-code
|
||||
```
|
||||
|
||||
### 第二步:安裝規則(必需)
|
||||
@@ -95,7 +95,7 @@ cp -r everything-claude-code/rules/* ~/.claude/rules/
|
||||
# /plan "新增使用者認證"
|
||||
|
||||
# 查看可用指令
|
||||
/plugin list ecc@ecc
|
||||
/plugin list everything-claude-code@everything-claude-code
|
||||
```
|
||||
|
||||
**完成!** 您現在使用 15+ 代理程式、30+ 技能和 20+ 指令。
|
||||
@@ -270,7 +270,7 @@ 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`:
|
||||
@@ -286,7 +286,7 @@ everything-claude-code/
|
||||
}
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"ecc@ecc": true
|
||||
"everything-claude-code@everything-claude-code": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const CATEGORIES = [
|
||||
@@ -187,7 +188,7 @@ function detectTargetMode(rootDir) {
|
||||
}
|
||||
|
||||
function findPluginInstall(rootDir) {
|
||||
const homeDir = process.env.HOME || '';
|
||||
const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir() || '';
|
||||
const pluginDirs = [
|
||||
'ecc',
|
||||
'ecc@ecc',
|
||||
|
||||
@@ -27,13 +27,12 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 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 SESSION_ID = process.env.CLAUDE_SESSION_ID || process.env.ECC_SESSION_ID || `pid-${process.ppid || process.pid}`;
|
||||
const STATE_FILE = path.join(STATE_DIR, `state-${SESSION_ID.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`);
|
||||
let activeStateFile = null;
|
||||
|
||||
// State expires after 30 minutes of inactivity
|
||||
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
||||
const READ_HEARTBEAT_MS = 60 * 1000;
|
||||
|
||||
// Maximum checked entries to prevent unbounded growth
|
||||
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) ---
|
||||
|
||||
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() {
|
||||
const stateFile = getStateFile();
|
||||
try {
|
||||
if (fs.existsSync(STATE_FILE)) {
|
||||
const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
||||
if (fs.existsSync(stateFile)) {
|
||||
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||
const lastActive = state.last_active || 0;
|
||||
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 state;
|
||||
@@ -75,15 +126,30 @@ function pruneCheckedEntries(checked) {
|
||||
}
|
||||
|
||||
function saveState(state) {
|
||||
const stateFile = getStateFile();
|
||||
let tmpFile = null;
|
||||
try {
|
||||
state.last_active = Date.now();
|
||||
state.checked = pruneCheckedEntries(state.checked);
|
||||
fs.mkdirSync(STATE_DIR, { recursive: true });
|
||||
// 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.renameSync(tmpFile, STATE_FILE);
|
||||
} catch (_) { /* ignore */ }
|
||||
try {
|
||||
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) {
|
||||
@@ -97,7 +163,9 @@ function markChecked(key) {
|
||||
function isChecked(key) {
|
||||
const state = loadState();
|
||||
const found = state.checked.includes(key);
|
||||
saveState(state);
|
||||
if (found && Date.now() - (state.last_active || 0) > READ_HEARTBEAT_MS) {
|
||||
saveState(state);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -109,9 +177,13 @@ function isChecked(key) {
|
||||
for (const f of files) {
|
||||
if (!f.startsWith('state-') || !f.endsWith('.json')) continue;
|
||||
const fp = path.join(STATE_DIR, f);
|
||||
const stat = fs.statSync(fp);
|
||||
if (now - stat.mtimeMs > SESSION_TIMEOUT_MS * 2) {
|
||||
fs.unlinkSync(fp);
|
||||
try {
|
||||
const stat = fs.statSync(fp);
|
||||
if (now - stat.mtimeMs > SESSION_TIMEOUT_MS * 2) {
|
||||
fs.unlinkSync(fp);
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore files that disappear between readdir/stat/unlink.
|
||||
}
|
||||
}
|
||||
} catch (_) { /* ignore */ }
|
||||
@@ -125,15 +197,62 @@ function sanitizePath(filePath) {
|
||||
for (const char of String(filePath || '')) {
|
||||
const code = char.codePointAt(0);
|
||||
const isAsciiControl = code <= 0x1f || code === 0x7f;
|
||||
const isBidiOverride =
|
||||
(code >= 0x200e && code <= 0x200f) ||
|
||||
(code >= 0x202a && code <= 0x202e) ||
|
||||
(code >= 0x2066 && code <= 0x2069);
|
||||
sanitized += isAsciiControl || isBidiOverride ? ' ' : char;
|
||||
const isBidiOverride = (code >= 0x200e && code <= 0x200f) || (code >= 0x202a && code <= 0x202e) || (code >= 0x2066 && code <= 0x2069);
|
||||
sanitized += (isAsciiControl || isBidiOverride) ? ' ' : char;
|
||||
}
|
||||
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 ---
|
||||
|
||||
function editGateMsg(filePath) {
|
||||
@@ -215,6 +334,8 @@ function run(rawInput) {
|
||||
} catch (_) {
|
||||
return rawInput; // allow on parse error
|
||||
}
|
||||
activeStateFile = null;
|
||||
getStateFile(data);
|
||||
|
||||
const rawToolName = data.tool_name || '';
|
||||
const toolInput = data.tool_input || {};
|
||||
@@ -224,7 +345,7 @@ function run(rawInput) {
|
||||
|
||||
if (toolName === 'Edit' || toolName === 'Write') {
|
||||
const filePath = toolInput.file_path || '';
|
||||
if (!filePath) {
|
||||
if (!filePath || isClaudeSettingsPath(filePath)) {
|
||||
return rawInput; // allow
|
||||
}
|
||||
|
||||
@@ -240,7 +361,7 @@ function run(rawInput) {
|
||||
const edits = toolInput.edits || [];
|
||||
for (const edit of edits) {
|
||||
const filePath = edit.file_path || '';
|
||||
if (filePath && !isChecked(filePath)) {
|
||||
if (filePath && !isClaudeSettingsPath(filePath) && !isChecked(filePath)) {
|
||||
markChecked(filePath);
|
||||
return denyResult(editGateMsg(filePath));
|
||||
}
|
||||
@@ -250,6 +371,9 @@ function run(rawInput) {
|
||||
|
||||
if (toolName === 'Bash') {
|
||||
const command = toolInput.command || '';
|
||||
if (isReadOnlyGitIntrospection(command)) {
|
||||
return rawInput;
|
||||
}
|
||||
|
||||
if (DESTRUCTIVE_BASH.test(command)) {
|
||||
// Gate destructive commands on first attempt; allow retry after facts presented
|
||||
|
||||
@@ -53,11 +53,11 @@ module.exports = createInstallTargetAdapter({
|
||||
}));
|
||||
}).sort((left, right) => {
|
||||
const getPriority = value => {
|
||||
if (value === 'rules') {
|
||||
if (value === '.cursor') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (value === '.cursor') {
|
||||
if (value === 'rules') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ An interactive, step-by-step installation wizard for the Everything Claude Code
|
||||
## Prerequisites
|
||||
|
||||
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"
|
||||
|
||||
---
|
||||
|
||||
@@ -10,10 +10,12 @@ const { spawnSync } = require('child_process');
|
||||
const runner = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'run-with-flags.js');
|
||||
const externalStateDir = process.env.GATEGUARD_STATE_DIR;
|
||||
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
|
||||
const TEST_SESSION_ID = 'gateguard-test-session';
|
||||
const stateFile = path.join(stateDir, `state-${TEST_SESSION_ID}.json`);
|
||||
const READ_HEARTBEAT_MS = 60 * 1000;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
@@ -29,11 +31,12 @@ function test(name, fn) {
|
||||
|
||||
function clearState() {
|
||||
try {
|
||||
if (fs.existsSync(stateFile)) {
|
||||
fs.unlinkSync(stateFile);
|
||||
if (fs.existsSync(stateDir)) {
|
||||
fs.rmSync(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(stateDir, { recursive: true });
|
||||
} 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++;
|
||||
|
||||
// --- Test 12: reads refresh active session state ---
|
||||
// --- Test 12: hot-path reads do not rewrite state within heartbeat ---
|
||||
clearState();
|
||||
if (test('touches last_active on read so active sessions do not age out', () => {
|
||||
const staleButActive = Date.now() - (29 * 60 * 1000);
|
||||
if (test('does not rewrite state on hot-path reads within heartbeat window', () => {
|
||||
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({
|
||||
checked: ['/src/keep-alive.js'],
|
||||
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({
|
||||
tool_name: 'Edit',
|
||||
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'));
|
||||
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++;
|
||||
|
||||
// --- Test 13: pruning preserves routine bash gate marker ---
|
||||
// --- Test 14: pruning preserves routine bash gate marker ---
|
||||
clearState();
|
||||
if (test('preserves __bash_session__ when pruning oversized state', () => {
|
||||
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');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Cleanup only the temp directory created by this test file.
|
||||
if (!externalStateDir) {
|
||||
try {
|
||||
if (fs.existsSync(stateDir)) {
|
||||
fs.rmSync(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(` [cleanup] failed to remove ${stateDir}: ${err.message}`);
|
||||
// --- 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++;
|
||||
|
||||
// --- 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`);
|
||||
|
||||
@@ -124,11 +124,11 @@ function runTests() {
|
||||
);
|
||||
assert.ok(
|
||||
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.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++;
|
||||
|
||||
|
||||
@@ -94,14 +94,14 @@ function runTests() {
|
||||
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/hooks.json'
|
||||
));
|
||||
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.strictEqual(hooksJson.strategy, 'preserve-relative-path');
|
||||
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.destinationPath,
|
||||
@@ -236,8 +236,8 @@ function runTests() {
|
||||
assert.strictEqual(commonAgentsDestinations.length, 1, 'Should keep only one common-agents.mdc operation');
|
||||
assert.strictEqual(
|
||||
normalizedRelativePath(commonAgentsDestinations[0].sourceRelativePath),
|
||||
'rules/common/agents.md',
|
||||
'Should prefer rules-core when cursor platform rules would collide'
|
||||
'.cursor/rules/common-agents.md',
|
||||
'Should prefer native .cursor/rules content when cursor platform rules would collide'
|
||||
);
|
||||
})) 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 packageLock = loadJsonObject(packageLockPath, '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);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
const readme = fs.readFileSync(zhCnReadmePath, 'utf8');
|
||||
const match = readme.match(/^\| \*\*版本\*\* \| 插件 \| 插件 \| 参考配置 \| ([0-9][0-9.]*) \|$/m);
|
||||
|
||||
@@ -19,12 +19,21 @@ function cleanup(dirPath) {
|
||||
}
|
||||
|
||||
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], {
|
||||
cwd: options.cwd || path.join(__dirname, '..', '..'),
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: options.homeDir || process.env.HOME,
|
||||
},
|
||||
env,
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 10000,
|
||||
@@ -132,6 +141,109 @@ function runTests() {
|
||||
}
|
||||
})) 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}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user