mirror of
https://github.com/Jeuners/ECC.git
synced 2026-09-09 15:02:30 +02:00
* fix(ci): resync lockfiles with package.json (eslint 10) + migrate yarn.lock to Yarn 4 format package.json requires eslint@^10.6.0 but the committed locks pinned 9.39.2, so npm ci aborted and Yarn 4 hardened mode rejected the stale v1-classic yarn.lock (YN0028). Regenerate package-lock.json and rewrite yarn.lock in Yarn 4 (berry) format so npm ci and immutable yarn installs both pass. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(ci): require clean probe exit for Windows shell/bash detection; add pyyaml dev dep Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor: consolidate duplicated hook-root resolver into shared resolveEccRoot() (#2368) The inline node -e resolver blob was duplicated ~60x across hooks.json, command docs, and translations. Each copy inlined the full ~700-char plugin-root search using a spread over nested array literals (p.join(d,'plugins',...s) over [['ecc'],...]), which breaks Windows hook execution due to shell quoting (#2368). Collapse every copy to a 250-char locator that loads the committed resolve-ecc-root module and delegates to resolveEccRoot() — no spread, no nested array literals, no escaped double quotes. The real search logic now lives in one tested module. Also route session-start-bootstrap.js through resolveEccRoot() instead of its own duplicated reimplementation, and fix the auto-update.md 'marketplace' (singular) typo along the way. Guard tests updated: discovery behavior is asserted against resolveEccRoot(); the inline is asserted to delegate and to contain no Windows-fragile constructs. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(resolve-ecc-root): restore full env-unset discovery in inline resolver Address Greptile review on #2410: when CLAUDE_PLUGIN_ROOT is unset the delegating inline could only load the resolver module from ~/.claude, returning ~/.claude without ever reaching the plugin/cache search. Restore the old inline's discovery breadth (exact plugin roots + versioned cache) Windows-safely (no spread, nested arrays, or escaped quotes), then delegate the authoritative decision to resolveEccRoot(). Add regression tests for plugin-subdir and versioned-cache bootstrap with env unset. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: affaan <affaan@itomarkets.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
6fac227f7a
commit
3af4676e99
22 changed files with 222 additions and 204 deletions
|
|
@ -29,18 +29,7 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const CURRENT_PLUGIN_SLUG = 'ecc';
|
||||
const LEGACY_PLUGIN_SLUG = 'everything-claude-code';
|
||||
const KNOWN_PLUGIN_PATHS = [
|
||||
[CURRENT_PLUGIN_SLUG],
|
||||
[`${CURRENT_PLUGIN_SLUG}@${CURRENT_PLUGIN_SLUG}`],
|
||||
['marketplaces', CURRENT_PLUGIN_SLUG],
|
||||
[LEGACY_PLUGIN_SLUG],
|
||||
[`${LEGACY_PLUGIN_SLUG}@${LEGACY_PLUGIN_SLUG}`],
|
||||
['marketplaces', LEGACY_PLUGIN_SLUG],
|
||||
];
|
||||
const CACHE_PLUGIN_SLUGS = [CURRENT_PLUGIN_SLUG, LEGACY_PLUGIN_SLUG];
|
||||
const { resolveEccRoot } = require('../lib/resolve-ecc-root');
|
||||
|
||||
// Read the raw JSON event from stdin
|
||||
const raw = fs.readFileSync(0, 'utf8');
|
||||
|
|
@ -48,74 +37,9 @@ const raw = fs.readFileSync(0, 'utf8');
|
|||
// Path (relative to plugin root) to the hook runner
|
||||
const rel = path.join('scripts', 'hooks', 'run-with-flags.js');
|
||||
|
||||
/**
|
||||
* Returns true when `candidate` looks like a valid ECC plugin root, i.e. the
|
||||
* run-with-flags.js runner exists inside it.
|
||||
*
|
||||
* @param {unknown} candidate
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasRunnerRoot(candidate) {
|
||||
const value = typeof candidate === 'string' ? candidate.trim() : '';
|
||||
return value.length > 0 && fs.existsSync(path.join(path.resolve(value), rel));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the ECC plugin root using the following priority order:
|
||||
* 1. CLAUDE_PLUGIN_ROOT environment variable
|
||||
* 2. ~/.claude (direct install)
|
||||
* 3. Several well-known plugin sub-paths under ~/.claude/plugins/ (current + legacy)
|
||||
* 4. Versioned cache directories under ~/.claude/plugins/cache/{ecc,everything-claude-code}/
|
||||
* 5. Falls back to ~/.claude if nothing else matches
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
function resolvePluginRoot() {
|
||||
const envRoot = process.env.CLAUDE_PLUGIN_ROOT || '';
|
||||
if (hasRunnerRoot(envRoot)) {
|
||||
return path.resolve(envRoot.trim());
|
||||
}
|
||||
|
||||
const home = require('os').homedir();
|
||||
const claudeDir = path.join(home, '.claude');
|
||||
|
||||
if (hasRunnerRoot(claudeDir)) {
|
||||
return claudeDir;
|
||||
}
|
||||
|
||||
const knownPaths = KNOWN_PLUGIN_PATHS.map((segments) =>
|
||||
path.join(claudeDir, 'plugins', ...segments)
|
||||
);
|
||||
|
||||
for (const candidate of knownPaths) {
|
||||
if (hasRunnerRoot(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk versioned cache: ~/.claude/plugins/cache/{ecc,everything-claude-code}/<org>/<version>/
|
||||
try {
|
||||
for (const slug of CACHE_PLUGIN_SLUGS) {
|
||||
const cacheBase = path.join(claudeDir, 'plugins', 'cache', slug);
|
||||
for (const org of fs.readdirSync(cacheBase, { withFileTypes: true })) {
|
||||
if (!org.isDirectory()) continue;
|
||||
for (const version of fs.readdirSync(path.join(cacheBase, org.name), { withFileTypes: true })) {
|
||||
if (!version.isDirectory()) continue;
|
||||
const candidate = path.join(cacheBase, org.name, version.name);
|
||||
if (hasRunnerRoot(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// cache directory may not exist; that's fine
|
||||
}
|
||||
|
||||
return claudeDir;
|
||||
}
|
||||
|
||||
const root = resolvePluginRoot();
|
||||
// Resolve the ECC plugin root via the shared resolver, probing for the runner
|
||||
// so a valid root is one that actually contains run-with-flags.js.
|
||||
const root = resolveEccRoot({ probe: rel });
|
||||
const script = path.join(root, rel);
|
||||
|
||||
if (fs.existsSync(script)) {
|
||||
|
|
|
|||
|
|
@ -100,32 +100,27 @@ function resolveEccRoot(options = {}) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Compact inline version for embedding in command .md code blocks.
|
||||
* Compact inline locator for embedding in hooks.json and command .md code blocks.
|
||||
*
|
||||
* This is the minified form of resolveEccRoot() suitable for use in
|
||||
* node -e "..." scripts where require() is not available before the
|
||||
* root is known.
|
||||
* Earlier revisions inlined the *entire* resolveEccRoot() search (~700 chars,
|
||||
* duplicated ~80×). That blob used a spread (`...s`) over nested array literals,
|
||||
* which broke Windows hook execution due to shell quoting (#2368).
|
||||
*
|
||||
* This minified form contains no spread, no nested array literals, and no
|
||||
* escaped double quotes, so it survives `node -e "..."` quoting on every shell.
|
||||
* When CLAUDE_PLUGIN_ROOT is set (as Claude Code does for plugin hooks and
|
||||
* commands) it is used directly. Otherwise the inline probes the same set of
|
||||
* locations resolveEccRoot() knows about — ~/.claude, the exact plugin roots
|
||||
* under ~/.claude/plugins/, and the versioned plugin cache — only far enough to
|
||||
* load the committed resolve-ecc-root module, then delegates the authoritative
|
||||
* decision to resolveEccRoot(). This keeps discovery behaviour identical to the
|
||||
* old inline while centralising the real logic in one tested module.
|
||||
*
|
||||
* Usage in commands:
|
||||
* const _r = <paste INLINE_RESOLVE>;
|
||||
* const sm = require(_r + '/scripts/lib/session-manager');
|
||||
*/
|
||||
function inlineSingleQuote(value) {
|
||||
return `'${String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
||||
}
|
||||
|
||||
function inlineArray(values) {
|
||||
return `[${values.map(inlineSingleQuote).join(',')}]`;
|
||||
}
|
||||
|
||||
function inlineNestedArray(values) {
|
||||
return `[${values.map(inlineArray).join(',')}]`;
|
||||
}
|
||||
|
||||
const INLINE_PLUGIN_ROOT_SEGMENTS = inlineNestedArray(PLUGIN_ROOT_SEGMENTS);
|
||||
const INLINE_PLUGIN_CACHE_SLUGS = inlineArray(PLUGIN_CACHE_SLUGS);
|
||||
|
||||
const INLINE_RESOLVE = `(()=>{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 ${INLINE_PLUGIN_ROOT_SEGMENTS}){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of ${INLINE_PLUGIN_CACHE_SLUGS}){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})()`;
|
||||
const INLINE_RESOLVE = `(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})()`;
|
||||
|
||||
module.exports = {
|
||||
resolveEccRoot,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ const {
|
|||
log
|
||||
} = require('./utils');
|
||||
|
||||
function resolveCreatedTime(stats) {
|
||||
return stats.birthtimeMs > 0 ? stats.birthtime : stats.ctime;
|
||||
}
|
||||
|
||||
// Session filename pattern: YYYY-MM-DD-[session-id]-session.tmp
|
||||
// The session-id is optional (old format) and can include letters, digits,
|
||||
// underscores, and hyphens, but must not start with a hyphen.
|
||||
|
|
@ -116,7 +120,7 @@ function getSessionCandidates(options = {}) {
|
|||
hasContent: stats.size > 0,
|
||||
size: stats.size,
|
||||
modifiedTime: stats.mtime,
|
||||
createdTime: stats.birthtime || stats.ctime
|
||||
createdTime: resolveCreatedTime(stats)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -151,7 +155,7 @@ function buildSessionRecord(sessionPath, metadata) {
|
|||
hasContent: stats.size > 0,
|
||||
size: stats.size,
|
||||
modifiedTime: stats.mtime,
|
||||
createdTime: stats.birthtime || stats.ctime
|
||||
createdTime: resolveCreatedTime(stats)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue