7 Commits

Author SHA1 Message Date
Affaan Mustafa
f2fe7dc0d1 Merge branch 'main' into feat/auto-update-command 2026-04-14 19:29:36 -07:00
Affaan Mustafa
48a30b53c8 Merge pull request #1402 from affaan-m/docs/community-skill-highlights
docs: add community skill ecosystem notes
2026-04-14 19:28:57 -07:00
Affaan Mustafa
ecc5e0e2d6 Merge pull request #1432 from S1lverline/fix/harness-audit-marketplaces
fix(harness-audit): detect ECC plugin under marketplaces/ subdirectory
2026-04-14 19:13:24 -07:00
S1lverline
aa96279ecc fix(harness-audit): detect ECC plugin under marketplaces/ subdirectory
`findPluginInstall()` in `scripts/harness-audit.js` scans two candidate
roots:

  {rootDir}/.claude/plugins/
  {HOME}/.claude/plugins/

Current Claude Code marketplace installs live one directory deeper:

  {HOME}/.claude/plugins/marketplaces/{ecc,everything-claude-code}/...

As a result, running `node scripts/harness-audit.js repo` on any
consumer project reports `consumer-plugin-install: false` even when ECC
is fully installed via marketplace, costing 4 points from Tool Coverage.

Add the `marketplaces/` intermediate directory to `candidateRoots` so
both legacy and current install layouts are recognized. The change is
purely additive: existing candidate paths still resolve, and the new
ones only match when the marketplace layout is present.

Reproduction:
  1. Install ECC via Claude Code plugin marketplace
  2. cd into any consumer project
  3. node ~/.claude/plugins/marketplaces/everything-claude-code/scripts/harness-audit.js repo
  4. Observe consumer-plugin-install=false despite a working install
2026-04-14 23:37:10 +09:00
Affaan Mustafa
68ee51f1e3 docs: add community skill ecosystem notes 2026-04-13 00:45:51 -07:00
Affaan Mustafa
7b536552e8 test: normalize auto-update repo root expectation on windows 2026-04-13 00:38:12 -07:00
Affaan Mustafa
29497c0576 feat: add auto-update command 2026-04-13 00:31:20 -07:00
15 changed files with 849 additions and 30 deletions

View File

@@ -1,6 +1,6 @@
# Everything Claude Code (ECC) — Agent Instructions
This is a **production-ready AI coding plugin** providing 47 specialized agents, 181 skills, 79 commands, and automated hook workflows for software development.
This is a **production-ready AI coding plugin** providing 47 specialized agents, 181 skills, 80 commands, and automated hook workflows for software development.
**Version:** 1.10.0
@@ -147,7 +147,7 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
```
agents/ — 47 specialized subagents
skills/ — 181 workflow skills and domain knowledge
commands/ — 79 slash commands
commands/ — 80 slash commands
hooks/ — Trigger-based automations
rules/ — Always-follow guidelines (common + per-language)
scripts/ — Cross-platform Node.js utilities

View File

@@ -239,7 +239,7 @@ For manual install instructions see the README in the `rules/` folder. When copy
/plugin list ecc@ecc
```
**That's it!** You now have access to 47 agents, 181 skills, and 79 legacy command shims.
**That's it!** You now have access to 47 agents, 181 skills, and 80 legacy command shims.
### Dashboard GUI
@@ -1013,6 +1013,14 @@ Please contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
- Testing strategies (different frameworks, visual regression)
- Domain-specific knowledge (ML, data engineering, mobile)
### Community Ecosystem Notes
These are not bundled with ECC and are not audited by this repo, but they are worth knowing about if you are exploring the broader Claude Code skills ecosystem:
- [claude-seo](https://github.com/AgriciDaniel/claude-seo) — SEO-focused skill and agent collection
- [claude-ads](https://github.com/AgriciDaniel/claude-ads) — Ad-audit and paid-growth workflow collection
- [claude-cybersecurity](https://github.com/AgriciDaniel/claude-cybersecurity) — Security-oriented skill and agent collection
---
## Cursor IDE Support
@@ -1198,7 +1206,7 @@ The configuration is automatically detected from `.opencode/opencode.json`.
| Feature | Claude Code | OpenCode | Status |
|---------|-------------|----------|--------|
| Agents | PASS: 47 agents | PASS: 12 agents | **Claude Code leads** |
| Commands | PASS: 79 commands | PASS: 31 commands | **Claude Code leads** |
| Commands | PASS: 80 commands | PASS: 31 commands | **Claude Code leads** |
| Skills | PASS: 181 skills | PASS: 37 skills | **Claude Code leads** |
| Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** |
| Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** |
@@ -1307,7 +1315,7 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e
| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode |
|---------|------------|------------|-----------|----------|
| **Agents** | 47 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |
| **Commands** | 79 | Shared | Instruction-based | 31 |
| **Commands** | 80 | Shared | Instruction-based | 31 |
| **Skills** | 181 | Shared | 10 (native format) | 37 |
| **Hook Events** | 8 types | 15 types | None yet | 11 types |
| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks |

View File

@@ -162,7 +162,7 @@ npx ecc-install typescript
/plugin list ecc@ecc
```
**完成!** 你现在可以使用 47 个代理、181 个技能和 79 个命令。
**完成!** 你现在可以使用 47 个代理、181 个技能和 80 个命令。
### multi-* 命令需要额外配置

View File

@@ -146,6 +146,7 @@ skills:
commands:
- agent-sort
- aside
- auto-update
- build-fix
- checkpoint
- claw

28
commands/auto-update.md Normal file
View File

@@ -0,0 +1,28 @@
---
description: Pull the latest ECC repo changes and reinstall the current managed targets.
disable-model-invocation: true
---
# Auto Update
Update ECC from its upstream repo and regenerate the current context's managed install using the original install-state request.
## Usage
```bash
# Preview the update without mutating anything
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [['ecc'],['ecc@ecc'],['marketplace','ecc'],['everything-claude-code'],['everything-claude-code@everything-claude-code'],['marketplace','everything-claude-code']]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of ['ecc','everything-claude-code']){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();console.log(r)")}"
node "$ECC_ROOT/scripts/auto-update.js" --dry-run
# Update only Cursor-managed files in the current project
node "$ECC_ROOT/scripts/auto-update.js" --target cursor
# Override the ECC repo root explicitly
node "$ECC_ROOT/scripts/auto-update.js" --repo-root /path/to/everything-claude-code
```
## Notes
- This command uses the recorded install-state request and reruns `install-apply.js` after pulling the latest repo changes.
- Reinstall is intentional: it handles upstream renames and deletions that `repair.js` cannot safely reconstruct from stale operations alone.
- Use `--dry-run` first if you want to see the reconstructed reinstall plan before mutating anything.

View File

@@ -1,6 +1,6 @@
# Everything Claude Code (ECC) — 智能体指令
这是一个**生产就绪的 AI 编码插件**,提供 47 个专业代理、181 项技能、79 条命令以及自动化钩子工作流,用于软件开发。
这是一个**生产就绪的 AI 编码插件**,提供 47 个专业代理、181 项技能、80 条命令以及自动化钩子工作流,用于软件开发。
**版本:** 1.10.0
@@ -148,7 +148,7 @@
```
agents/ — 47 个专业子代理
skills/ — 181 个工作流技能和领域知识
commands/ — 79 个斜杠命令
commands/ — 80 个斜杠命令
hooks/ — 基于触发的自动化
rules/ — 始终遵循的指导方针(通用 + 每种语言)
scripts/ — 跨平台 Node.js 实用工具

View File

@@ -209,7 +209,7 @@ npx ecc-install typescript
/plugin list ecc@ecc
```
**搞定!** 你现在可以使用 47 个智能体、181 项技能和 79 个命令了。
**搞定!** 你现在可以使用 47 个智能体、181 项技能和 80 个命令了。
***
@@ -1095,7 +1095,7 @@ opencode
| 功能特性 | Claude Code | OpenCode | 状态 |
|---------|-------------|----------|--------|
| 智能体 | PASS: 47 个 | PASS: 12 个 | **Claude Code 领先** |
| 命令 | PASS: 79 个 | PASS: 31 个 | **Claude Code 领先** |
| 命令 | PASS: 80 个 | PASS: 31 个 | **Claude Code 领先** |
| 技能 | PASS: 181 项 | PASS: 37 项 | **Claude Code 领先** |
| 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
@@ -1207,7 +1207,7 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以
| 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode |
|---------|------------|------------|-----------|----------|
| **智能体** | 47 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
| **命令** | 79 | 共享 | 基于指令 | 31 |
| **命令** | 80 | 共享 | 基于指令 | 31 |
| **技能** | 181 | 共享 | 10 (原生格式) | 37 |
| **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 |
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 |

View File

@@ -90,6 +90,7 @@
".gemini",
".opencode",
"mcp-configs",
"scripts/auto-update.js",
"scripts/setup-package-manager.js"
],
"targets": [

View File

@@ -259,11 +259,11 @@
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@opencode-ai/plugin": "^1.4.3",
"@opencode-ai/plugin": "^1.0.0",
"@types/node": "^20.19.24",
"c8": "^11.0.0",
"eslint": "^9.39.2",
"globals": "^17.5.0",
"globals": "^17.4.0",
"markdownlint-cli": "^0.48.0",
"typescript": "^5.9.3"
},

361
scripts/auto-update.js Normal file
View File

@@ -0,0 +1,361 @@
#!/usr/bin/env node
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const { discoverInstalledStates } = require('./lib/install-lifecycle');
const { SUPPORTED_INSTALL_TARGETS } = require('./lib/install-manifests');
function showHelp(exitCode = 0) {
console.log(`
Usage: node scripts/auto-update.js [--target <${SUPPORTED_INSTALL_TARGETS.join('|')}>] [--repo-root <path>] [--dry-run] [--json]
Pull the latest ECC repo changes and reinstall the current context's managed targets
using the original install-state request.
`);
process.exit(exitCode);
}
function parseArgs(argv) {
const args = argv.slice(2);
const parsed = {
targets: [],
repoRoot: null,
dryRun: false,
json: false,
help: false,
};
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === '--target') {
parsed.targets.push(args[index + 1] || null);
index += 1;
} else if (arg === '--repo-root') {
parsed.repoRoot = args[index + 1] || null;
index += 1;
} else if (arg === '--dry-run') {
parsed.dryRun = true;
} else if (arg === '--json') {
parsed.json = true;
} else if (arg === '--help' || arg === '-h') {
parsed.help = true;
} else {
throw new Error(`Unknown argument: ${arg}`);
}
}
return parsed;
}
function deriveRepoRootFromState(state) {
const operations = Array.isArray(state && state.operations) ? state.operations : [];
for (const operation of operations) {
if (typeof operation.sourcePath !== 'string' || !operation.sourcePath.trim()) {
continue;
}
if (typeof operation.sourceRelativePath !== 'string' || !operation.sourceRelativePath.trim()) {
continue;
}
const relativeParts = operation.sourceRelativePath
.split(/[\\/]+/)
.filter(Boolean);
if (relativeParts.length === 0) {
continue;
}
let repoRoot = path.resolve(operation.sourcePath);
for (let index = 0; index < relativeParts.length; index += 1) {
repoRoot = path.dirname(repoRoot);
}
return repoRoot;
}
throw new Error('Unable to infer ECC repo root from install-state operations');
}
function buildInstallApplyArgs(record) {
const state = record.state;
const target = state.target.target || record.adapter.target;
const request = state.request || {};
const args = [];
if (target) {
args.push('--target', target);
}
if (request.profile) {
args.push('--profile', request.profile);
}
if (Array.isArray(request.modules) && request.modules.length > 0) {
args.push('--modules', request.modules.join(','));
}
for (const componentId of Array.isArray(request.includeComponents) ? request.includeComponents : []) {
args.push('--with', componentId);
}
for (const componentId of Array.isArray(request.excludeComponents) ? request.excludeComponents : []) {
args.push('--without', componentId);
}
for (const language of Array.isArray(request.legacyLanguages) ? request.legacyLanguages : []) {
args.push(language);
}
return args;
}
function determineInstallCwd(record, repoRoot) {
if (record.adapter.kind === 'project') {
return path.dirname(record.state.target.root);
}
return repoRoot;
}
function validateRepoRoot(repoRoot) {
const normalized = path.resolve(repoRoot);
const packageJsonPath = path.join(normalized, 'package.json');
const installApplyPath = path.join(normalized, 'scripts', 'install-apply.js');
if (!fs.existsSync(packageJsonPath)) {
throw new Error(`Invalid ECC repo root: missing package.json at ${packageJsonPath}`);
}
if (!fs.existsSync(installApplyPath)) {
throw new Error(`Invalid ECC repo root: missing install script at ${installApplyPath}`);
}
return normalized;
}
function runExternalCommand(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: options.cwd,
env: options.env || process.env,
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
});
if (result.error) {
throw result.error;
}
if (typeof result.status === 'number' && result.status !== 0) {
const errorOutput = (result.stderr || result.stdout || '').trim();
throw new Error(`${command} ${args.join(' ')} failed${errorOutput ? `: ${errorOutput}` : ''}`);
}
return result;
}
function runAutoUpdate(options = {}, dependencies = {}) {
const discover = dependencies.discoverInstalledStates || discoverInstalledStates;
const execute = dependencies.runExternalCommand || runExternalCommand;
const homeDir = options.homeDir || process.env.HOME || os.homedir();
const projectRoot = options.projectRoot || process.cwd();
const requestedRepoRoot = options.repoRoot ? validateRepoRoot(options.repoRoot) : null;
const records = discover({
homeDir,
projectRoot,
targets: options.targets,
}).filter(record => record.exists);
const results = [];
if (records.length === 0) {
return {
dryRun: Boolean(options.dryRun),
repoRoot: requestedRepoRoot,
results,
summary: {
checkedCount: 0,
updatedCount: 0,
errorCount: 0,
},
};
}
const validRecords = [];
const inferredRepoRoots = [];
for (const record of records) {
if (record.error || !record.state) {
results.push({
adapter: record.adapter,
installStatePath: record.installStatePath,
status: 'error',
error: record.error || 'No valid install-state available',
});
continue;
}
const recordRepoRoot = requestedRepoRoot || validateRepoRoot(deriveRepoRootFromState(record.state));
inferredRepoRoots.push(recordRepoRoot);
validRecords.push({
record,
repoRoot: recordRepoRoot,
});
}
if (!requestedRepoRoot) {
const uniqueRepoRoots = [...new Set(inferredRepoRoots)];
if (uniqueRepoRoots.length > 1) {
throw new Error(`Multiple ECC repo roots detected: ${uniqueRepoRoots.join(', ')}`);
}
}
const repoRoot = requestedRepoRoot || inferredRepoRoots[0] || null;
if (!repoRoot) {
return {
dryRun: Boolean(options.dryRun),
repoRoot,
results,
summary: {
checkedCount: results.length,
updatedCount: 0,
errorCount: results.length,
},
};
}
const env = {
...process.env,
HOME: homeDir,
USERPROFILE: homeDir,
};
if (!options.dryRun) {
execute('git', ['fetch', '--all', '--prune'], { cwd: repoRoot, env });
execute('git', ['pull', '--ff-only'], { cwd: repoRoot, env });
}
for (const entry of validRecords) {
const installArgs = buildInstallApplyArgs(entry.record);
const args = [
path.join(repoRoot, 'scripts', 'install-apply.js'),
...installArgs,
'--json',
];
if (options.dryRun) {
args.push('--dry-run');
}
try {
const commandResult = execute(process.execPath, args, {
cwd: determineInstallCwd(entry.record, repoRoot),
env,
});
let payload = null;
if (commandResult.stdout && commandResult.stdout.trim()) {
payload = JSON.parse(commandResult.stdout);
}
results.push({
adapter: entry.record.adapter,
installStatePath: entry.record.installStatePath,
repoRoot,
cwd: determineInstallCwd(entry.record, repoRoot),
installArgs,
status: options.dryRun ? 'planned' : 'updated',
payload,
});
} catch (error) {
results.push({
adapter: entry.record.adapter,
installStatePath: entry.record.installStatePath,
repoRoot,
installArgs,
status: 'error',
error: error.message,
});
}
}
return {
dryRun: Boolean(options.dryRun),
repoRoot,
results,
summary: {
checkedCount: results.length,
updatedCount: results.filter(result => result.status === 'updated' || result.status === 'planned').length,
errorCount: results.filter(result => result.status === 'error').length,
},
};
}
function printHuman(result) {
if (result.results.length === 0) {
console.log('No ECC install-state files found for the current home/project context.');
return;
}
console.log(`${result.dryRun ? 'Auto-update dry run' : 'Auto-update summary'}:\n`);
if (result.repoRoot) {
console.log(`Repo root: ${result.repoRoot}\n`);
}
for (const entry of result.results) {
console.log(`- ${entry.adapter.id}`);
console.log(` Status: ${entry.status.toUpperCase()}`);
console.log(` Install-state: ${entry.installStatePath}`);
if (entry.error) {
console.log(` Error: ${entry.error}`);
continue;
}
console.log(` Reinstall args: ${entry.installArgs.join(' ') || '(none)'}`);
}
console.log(`\nSummary: checked=${result.summary.checkedCount}, ${result.dryRun ? 'planned' : 'updated'}=${result.summary.updatedCount}, errors=${result.summary.errorCount}`);
}
function main() {
try {
const options = parseArgs(process.argv);
if (options.help) {
showHelp(0);
}
const result = runAutoUpdate({
homeDir: process.env.HOME || os.homedir(),
projectRoot: process.cwd(),
targets: options.targets,
repoRoot: options.repoRoot,
dryRun: options.dryRun,
});
if (options.json) {
console.log(JSON.stringify(result, null, 2));
} else {
printHuman(result);
}
process.exitCode = result.summary.errorCount > 0 ? 1 : 0;
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
}
if (require.main === module) {
main();
}
module.exports = {
parseArgs,
deriveRepoRootFromState,
buildInstallApplyArgs,
determineInstallCwd,
runAutoUpdate,
};

View File

@@ -33,6 +33,10 @@ const COMMANDS = {
script: 'repair.js',
description: 'Restore drifted or missing ECC-managed files',
},
'auto-update': {
script: 'auto-update.js',
description: 'Pull latest ECC changes and reinstall the current managed targets',
},
status: {
script: 'status.js',
description: 'Query the ECC SQLite state store status summary',
@@ -58,6 +62,7 @@ const PRIMARY_COMMANDS = [
'list-installed',
'doctor',
'repair',
'auto-update',
'status',
'sessions',
'session-inspect',
@@ -90,6 +95,7 @@ Examples:
ecc list-installed --json
ecc doctor --target cursor
ecc repair --dry-run
ecc auto-update --dry-run
ecc status --json
ecc sessions
ecc sessions session-active --json

View File

@@ -196,7 +196,9 @@ function findPluginInstall(rootDir) {
];
const candidateRoots = [
path.join(rootDir, '.claude', 'plugins'),
path.join(rootDir, '.claude', 'plugins', 'marketplaces'),
homeDir && path.join(homeDir, '.claude', 'plugins'),
homeDir && path.join(homeDir, '.claude', 'plugins', 'marketplaces'),
].filter(Boolean);
const candidates = candidateRoots.flatMap((pluginsDir) =>
pluginDirs.flatMap((pluginDir) => [

View File

@@ -0,0 +1,395 @@
/**
* Tests for scripts/auto-update.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
parseArgs,
deriveRepoRootFromState,
buildInstallApplyArgs,
determineInstallCwd,
runAutoUpdate,
} = require('../../scripts/auto-update');
const {
createInstallState,
} = require('../../scripts/lib/install-state');
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function cleanup(dirPath) {
fs.rmSync(dirPath, { recursive: true, force: true });
}
function makeRecord({ repoRoot, homeDir, projectRoot, adapter, request, resolution, operations }) {
const targetRoot = adapter.kind === 'project'
? path.join(projectRoot, `.${adapter.target}`)
: path.join(homeDir, '.claude');
const installStatePath = adapter.kind === 'project'
? path.join(targetRoot, 'ecc-install-state.json')
: path.join(targetRoot, 'ecc', 'install-state.json');
const state = createInstallState({
adapter,
targetRoot,
installStatePath,
request,
resolution,
operations,
source: {
repoVersion: '1.10.0',
repoCommit: 'abc123',
manifestVersion: 1,
},
});
return {
adapter,
targetRoot,
installStatePath,
exists: true,
state,
error: null,
repoRoot,
};
}
function ensureFakeRepo(repoRoot) {
fs.mkdirSync(path.join(repoRoot, 'scripts'), { recursive: true });
fs.writeFileSync(
path.join(repoRoot, 'package.json'),
JSON.stringify({ name: 'everything-claude-code', version: '1.10.0' }, null, 2)
);
fs.writeFileSync(path.join(repoRoot, 'scripts', 'install-apply.js'), '#!/usr/bin/env node\n');
}
function runTests() {
console.log('\n=== Testing auto-update.js ===\n');
let passed = 0;
let failed = 0;
if (test('parseArgs reads repo-root, target, dry-run, and json flags', () => {
const parsed = parseArgs([
'node',
'scripts/auto-update.js',
'--target',
'cursor',
'--repo-root',
'/tmp/ecc',
'--dry-run',
'--json',
]);
assert.deepStrictEqual(parsed.targets, ['cursor']);
assert.strictEqual(parsed.repoRoot, '/tmp/ecc');
assert.strictEqual(parsed.dryRun, true);
assert.strictEqual(parsed.json, true);
})) passed += 1; else failed += 1;
if (test('parseArgs rejects unknown arguments', () => {
assert.throws(
() => parseArgs(['node', 'scripts/auto-update.js', '--bogus']),
/Unknown argument: --bogus/
);
})) passed += 1; else failed += 1;
if (test('deriveRepoRootFromState uses sourcePath and sourceRelativePath', () => {
const state = {
operations: [
{
sourcePath: path.join('/tmp', 'ecc', 'scripts', 'setup-package-manager.js'),
sourceRelativePath: path.join('scripts', 'setup-package-manager.js'),
},
],
};
assert.strictEqual(
deriveRepoRootFromState(state),
path.resolve(path.join('/tmp', 'ecc'))
);
})) passed += 1; else failed += 1;
if (test('deriveRepoRootFromState fails when source metadata is unavailable', () => {
assert.throws(
() => deriveRepoRootFromState({ operations: [{ destinationPath: '/tmp/file' }] }),
/Unable to infer ECC repo root/
);
})) passed += 1; else failed += 1;
if (test('buildInstallApplyArgs reconstructs legacy installs', () => {
const record = {
adapter: { target: 'claude', kind: 'home' },
state: {
target: { target: 'claude' },
request: {
profile: null,
modules: [],
includeComponents: [],
excludeComponents: [],
legacyLanguages: ['typescript', 'python'],
legacyMode: true,
},
},
};
assert.deepStrictEqual(buildInstallApplyArgs(record), [
'--target', 'claude',
'typescript',
'python',
]);
})) passed += 1; else failed += 1;
if (test('buildInstallApplyArgs reconstructs manifest installs', () => {
const record = {
adapter: { target: 'cursor', kind: 'project' },
state: {
target: { target: 'cursor' },
request: {
profile: 'developer',
modules: ['platform-configs'],
includeComponents: ['component:alpha'],
excludeComponents: ['component:beta'],
legacyLanguages: [],
legacyMode: false,
},
},
};
assert.deepStrictEqual(buildInstallApplyArgs(record), [
'--target', 'cursor',
'--profile', 'developer',
'--modules', 'platform-configs',
'--with', 'component:alpha',
'--without', 'component:beta',
]);
})) passed += 1; else failed += 1;
if (test('determineInstallCwd uses the project root for project installs', () => {
const record = {
adapter: { kind: 'project' },
state: {
target: {
root: path.join('/tmp', 'project', '.cursor'),
},
},
};
assert.strictEqual(determineInstallCwd(record, '/tmp/ecc'), path.join('/tmp', 'project'));
})) passed += 1; else failed += 1;
if (test('runAutoUpdate reports when no install-state files are present', () => {
const result = runAutoUpdate(
{
homeDir: '/tmp/home',
projectRoot: '/tmp/project',
dryRun: true,
},
{
discoverInstalledStates: () => [],
}
);
assert.strictEqual(result.results.length, 0);
assert.strictEqual(result.summary.checkedCount, 0);
assert.strictEqual(result.summary.errorCount, 0);
})) passed += 1; else failed += 1;
if (test('runAutoUpdate rejects mixed inferred repo roots', () => {
const homeDir = createTempDir('auto-update-home-');
const projectRoot = createTempDir('auto-update-project-');
const repoOne = createTempDir('auto-update-repo-');
const repoTwo = createTempDir('auto-update-repo-');
try {
ensureFakeRepo(repoOne);
ensureFakeRepo(repoTwo);
const records = [
makeRecord({
repoRoot: repoOne,
homeDir,
projectRoot,
adapter: { id: 'claude-home', target: 'claude', kind: 'home' },
request: {
profile: null,
modules: [],
includeComponents: [],
excludeComponents: [],
legacyLanguages: ['typescript'],
legacyMode: true,
},
resolution: { selectedModules: ['legacy-claude-rules'], skippedModules: [] },
operations: [
{
kind: 'copy-file',
moduleId: 'legacy-claude-rules',
sourcePath: path.join(repoOne, 'rules', 'common', 'coding-style.md'),
sourceRelativePath: path.join('rules', 'common', 'coding-style.md'),
destinationPath: path.join(homeDir, '.claude', 'rules', 'common', 'coding-style.md'),
strategy: 'preserve-relative-path',
ownership: 'managed',
scaffoldOnly: false,
},
],
}),
makeRecord({
repoRoot: repoTwo,
homeDir,
projectRoot,
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
request: {
profile: 'core',
modules: [],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: { selectedModules: ['rules-core'], skippedModules: [] },
operations: [
{
kind: 'copy-file',
moduleId: 'rules-core',
sourcePath: path.join(repoTwo, '.cursor', 'mcp.json'),
sourceRelativePath: path.join('.cursor', 'mcp.json'),
destinationPath: path.join(projectRoot, '.cursor', 'mcp.json'),
strategy: 'sync-root-children',
ownership: 'managed',
scaffoldOnly: false,
},
],
}),
];
assert.throws(
() => runAutoUpdate(
{
homeDir,
projectRoot,
dryRun: true,
},
{
discoverInstalledStates: () => records,
}
),
/Multiple ECC repo roots detected/
);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(repoOne);
cleanup(repoTwo);
}
})) passed += 1; else failed += 1;
if (test('runAutoUpdate fetches, pulls, and reinstalls using reconstructed args', () => {
const homeDir = createTempDir('auto-update-home-');
const projectRoot = createTempDir('auto-update-project-');
const repoRoot = createTempDir('auto-update-repo-');
try {
ensureFakeRepo(repoRoot);
const records = [
makeRecord({
repoRoot,
homeDir,
projectRoot,
adapter: { id: 'cursor-project', target: 'cursor', kind: 'project' },
request: {
profile: 'developer',
modules: [],
includeComponents: ['component:alpha'],
excludeComponents: ['component:beta'],
legacyLanguages: [],
legacyMode: false,
},
resolution: { selectedModules: ['rules-core'], skippedModules: [] },
operations: [
{
kind: 'copy-file',
moduleId: 'platform-configs',
sourcePath: path.join(repoRoot, '.cursor', 'mcp.json'),
sourceRelativePath: path.join('.cursor', 'mcp.json'),
destinationPath: path.join(projectRoot, '.cursor', 'mcp.json'),
strategy: 'sync-root-children',
ownership: 'managed',
scaffoldOnly: false,
},
],
}),
];
const commands = [];
const result = runAutoUpdate(
{
homeDir,
projectRoot,
dryRun: false,
},
{
discoverInstalledStates: () => records,
runExternalCommand: (command, args, options) => {
commands.push({ command, args, options });
if (command === process.execPath) {
return {
stdout: JSON.stringify({
dryRun: false,
result: {
installStatePath: path.join(projectRoot, '.cursor', 'ecc-install-state.json'),
},
}),
stderr: '',
};
}
return { stdout: '', stderr: '' };
},
}
);
assert.strictEqual(result.summary.checkedCount, 1);
assert.strictEqual(result.summary.updatedCount, 1);
assert.deepStrictEqual(commands.map(entry => [entry.command, entry.args[0]]), [
['git', 'fetch'],
['git', 'pull'],
[process.execPath, path.join(repoRoot, 'scripts', 'install-apply.js')],
]);
assert.deepStrictEqual(commands[2].args.slice(1), [
'--target', 'cursor',
'--profile', 'developer',
'--with', 'component:alpha',
'--without', 'component:beta',
'--json',
]);
assert.strictEqual(commands[2].options.cwd, projectRoot);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(repoRoot);
}
})) passed += 1; else failed += 1;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();

View File

@@ -68,6 +68,7 @@ function main() {
assert.match(result.stdout, /catalog/);
assert.match(result.stdout, /list-installed/);
assert.match(result.stdout, /doctor/);
assert.match(result.stdout, /auto-update/);
}],
['delegates explicit install command', () => {
const result = runCli(['install', '--dry-run', '--json', 'typescript']);
@@ -112,6 +113,17 @@ function main() {
const payload = parseJson(result.stdout);
assert.deepStrictEqual(payload.records, []);
}],
['delegates auto-update command', () => {
const homeDir = createTempDir('ecc-cli-home-');
const projectRoot = createTempDir('ecc-cli-project-');
const result = runCli(['auto-update', '--dry-run', '--json'], {
cwd: projectRoot,
env: { HOME: homeDir },
});
assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout);
assert.deepStrictEqual(payload.results, []);
}],
['delegates session-inspect command', () => {
const homeDir = createTempDir('ecc-cli-home-');
const sessionsDir = path.join(homeDir, '.claude', 'sessions');
@@ -135,6 +147,11 @@ function main() {
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /Usage: node scripts\/repair\.js/);
}],
['supports help for the auto-update subcommand', () => {
const result = runCli(['help', 'auto-update']);
assert.strictEqual(result.status, 0, result.stderr);
assert.match(result.stdout, /Usage: node scripts\/auto-update\.js/);
}],
['supports help for the catalog subcommand', () => {
const result = runCli(['help', 'catalog']);
assert.strictEqual(result.status, 0, result.stderr);

View File

@@ -169,30 +169,30 @@ __metadata:
languageName: node
linkType: hard
"@opencode-ai/plugin@npm:^1.4.3":
version: 1.4.3
resolution: "@opencode-ai/plugin@npm:1.4.3"
"@opencode-ai/plugin@npm:^1.0.0":
version: 1.3.15
resolution: "@opencode-ai/plugin@npm:1.3.15"
dependencies:
"@opencode-ai/sdk": "npm:1.4.3"
"@opencode-ai/sdk": "npm:1.3.15"
zod: "npm:4.1.8"
peerDependencies:
"@opentui/core": ">=0.1.97"
"@opentui/solid": ">=0.1.97"
"@opentui/core": ">=0.1.96"
"@opentui/solid": ">=0.1.96"
peerDependenciesMeta:
"@opentui/core":
optional: true
"@opentui/solid":
optional: true
checksum: 10c0/a20328a691a674638e4718c1fb911ea68b60fc7560f1bf314324114ccdcabbddc12e98c4fc9f3aad69e92aaaac7edbd44216bf036955ec5d1f50282430ab06ae
checksum: 10c0/1a662ff700812223310612f3c8c7fd4465eda5763d726ec4d29d0eae26babf344ef176c9b987d79fe1e29c8a498178881a47d7080bb9f4db3e70dad59eb8cd9e
languageName: node
linkType: hard
"@opencode-ai/sdk@npm:1.4.3":
version: 1.4.3
resolution: "@opencode-ai/sdk@npm:1.4.3"
"@opencode-ai/sdk@npm:1.3.15":
version: 1.3.15
resolution: "@opencode-ai/sdk@npm:1.3.15"
dependencies:
cross-spawn: "npm:7.0.6"
checksum: 10c0/edba27ef01ecfb6fde7df2348f953aab64f2e7b99e9cd5b155474e7e02cc0db62da242d9edcd5b704110b9ef82bc16633d99d25eaa812d4279badede71ae419f
checksum: 10c0/3957ae62e0ec1e339d9493e03a2440c95afdd64a608a2dc9db8383338650318a294280b2142305db5b0147badacbefa0d07e949d31167e5a4a49c9d057d016fa
languageName: node
linkType: hard
@@ -548,12 +548,12 @@ __metadata:
dependencies:
"@eslint/js": "npm:^9.39.2"
"@iarna/toml": "npm:^2.2.5"
"@opencode-ai/plugin": "npm:^1.4.3"
"@opencode-ai/plugin": "npm:^1.0.0"
"@types/node": "npm:^20.19.24"
ajv: "npm:^8.18.0"
c8: "npm:^11.0.0"
eslint: "npm:^9.39.2"
globals: "npm:^17.5.0"
globals: "npm:^17.4.0"
markdownlint-cli: "npm:^0.48.0"
sql.js: "npm:^1.14.1"
typescript: "npm:^5.9.3"
@@ -834,10 +834,10 @@ __metadata:
languageName: node
linkType: hard
"globals@npm:^17.5.0":
version: 17.5.0
resolution: "globals@npm:17.5.0"
checksum: 10c0/92828102ed2f5637907725f0478038bed02fc83e9fc89300bb753639ba7c022b6c02576fc772117302b431b204591db1f2fa909d26f3f0a9852cc856a941df3f
"globals@npm:^17.4.0":
version: 17.4.0
resolution: "globals@npm:17.4.0"
checksum: 10c0/2be9e8c2b9035836f13d420b22f0247a328db82967d3bebfc01126d888ed609305f06c05895914e969653af5c6ba35fd7a0920f3e6c869afa60666c810630feb
languageName: node
linkType: hard