fix(commands): probe for auto-update.js in auto-update ECC_ROOT resolver (#2462)

The auto-update command's inline ECC_ROOT resolver delegates to
resolveEccRoot() with the default probe (scripts/lib/utils.js). A
hooks-runtime-only install copies scripts/lib/ into ~/.claude, so the
partial install satisfies the probe and shadows the full plugin root
under ~/.claude/plugins/marketplaces/. The command then fails with
MODULE_NOT_FOUND because ~/.claude/scripts/auto-update.js does not
exist.

Pass {probe: scripts/auto-update.js} so the resolver only accepts a
root that actually contains the script the command runs. Applied to
the command doc and its ja-JP/zh-CN translations, with regression
tests for both the resolver behavior and the embedded snippets.
This commit is contained in:
Karim Farahat
2026-08-04 00:28:59 -04:00
committed by GitHub
parent 7b76082b13
commit 203aac7710
5 changed files with 57 additions and 3 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ Update ECC from its upstream repo and regenerate the current context's managed i
```bash ```bash
# Preview the update without mutating anything # Preview the update without mutating anything
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}" ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot({probe:p.join('scripts','auto-update.js')})}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
node "$ECC_ROOT/scripts/auto-update.js" --dry-run node "$ECC_ROOT/scripts/auto-update.js" --dry-run
# Update only Cursor-managed files in the current project # Update only Cursor-managed files in the current project
+1 -1
View File
@@ -11,7 +11,7 @@ ECCをアップストリームリポジトリから更新し、元のインス
```bash ```bash
# 何も変更せずに更新をプレビュー # 何も変更せずに更新をプレビュー
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}" ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot({probe:p.join('scripts','auto-update.js')})}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
node "$ECC_ROOT/scripts/auto-update.js" --dry-run node "$ECC_ROOT/scripts/auto-update.js" --dry-run
# 現在のプロジェクトのCursor管理ファイルのみ更新 # 現在のプロジェクトのCursor管理ファイルのみ更新
+1 -1
View File
@@ -11,7 +11,7 @@ disable-model-invocation: true
```bash ```bash
# Preview the update without mutating anything # Preview the update without mutating anything
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}" ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot({probe:p.join('scripts','auto-update.js')})}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)")}"
node "$ECC_ROOT/scripts/auto-update.js" --dry-run node "$ECC_ROOT/scripts/auto-update.js" --dry-run
# Update only Cursor-managed files in the current project # Update only Cursor-managed files in the current project
+23
View File
@@ -50,6 +50,29 @@ test('instinct-status command uses shared inline resolver (no stale legacy fallb
); );
}); });
test('auto-update command probes for the script it runs, not just scripts/lib', () => {
// A partial install can carry full resolver evidence (script tree plus
// sentinel ECC skill, per #2544/#2577) yet still lack scripts/auto-update.js.
// The command's inline resolver must probe for the script it actually
// executes so such roots don't shadow the complete plugin root.
const autoUpdateDocs = [
path.join(__dirname, '..', '..', 'commands', 'auto-update.md'),
path.join(__dirname, '..', '..', 'docs', 'ja-JP', 'commands', 'auto-update.md'),
path.join(__dirname, '..', '..', 'docs', 'zh-CN', 'commands', 'auto-update.md'),
];
for (const docPath of autoUpdateDocs) {
const doc = fs.readFileSync(docPath, 'utf8');
assert.strictEqual(
(doc.match(/scripts','lib','resolve-ecc-root/g) || []).length, 1,
`${docPath} should embed the shared inline resolver`
);
assert.ok(
doc.includes("resolveEccRoot({probe:p.join('scripts','auto-update.js')})"),
`${docPath} should probe for scripts/auto-update.js`
);
}
});
test('resolveEccRoot module covers current and legacy marketplace plugin roots', () => { test('resolveEccRoot module covers current and legacy marketplace plugin roots', () => {
const { resolveEccRoot } = require('../../scripts/lib/resolve-ecc-root'); const { resolveEccRoot } = require('../../scripts/lib/resolve-ecc-root');
assert.ok(typeof resolveEccRoot === 'function'); assert.ok(typeof resolveEccRoot === 'function');
+31
View File
@@ -363,6 +363,37 @@ function runTests() {
} }
})) passed++; else failed++; })) passed++; else failed++;
if (test('custom probe skips a qualifying root that lacks the probed script (auto-update)', () => {
// The surviving failure shape after #2544/#2577: a partial install can
// carry full resolver evidence (script tree + sentinel ECC skill) yet
// still lack the top-level script that auto-update will execute. The
// default probe rightly accepts such a root; a caller probing for the
// script it runs must skip it and reach the complete plugin root.
const homeDir = createTempDir();
try {
const claudeDir = setupStandardInstall(homeDir);
const marketplaceRoot = setupLegacyPluginInstall(homeDir, ['marketplaces', 'ecc']);
fs.writeFileSync(path.join(marketplaceRoot, 'scripts', 'auto-update.js'), '// stub');
assert.strictEqual(
resolveEccRoot({ envRoot: '', homeDir }),
claudeDir,
'default probe accepts a root with full resolver evidence'
);
assert.strictEqual(
resolveEccRoot({
envRoot: '',
homeDir,
probe: path.join('scripts', 'auto-update.js'),
}),
marketplaceRoot,
'auto-update probe must skip roots that lack the script it will execute'
);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ─── INLINE_RESOLVE ─── // ─── INLINE_RESOLVE ───
if (test('INLINE_RESOLVE is a non-empty string', () => { if (test('INLINE_RESOLVE is a non-empty string', () => {