mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
fix: finish blocker lane hook and install regressions
This commit is contained in:
@@ -91,6 +91,7 @@
|
||||
"targets": [
|
||||
"claude",
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"codex",
|
||||
"opencode"
|
||||
],
|
||||
|
||||
@@ -73,7 +73,9 @@ function writeLegacySpawnOutput(raw, result) {
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(raw);
|
||||
if (Number.isInteger(result.status) && result.status === 0) {
|
||||
process.stdout.write(raw);
|
||||
}
|
||||
}
|
||||
|
||||
function getPluginRoot() {
|
||||
|
||||
@@ -85,7 +85,8 @@ function getSessionCandidates(options = {}) {
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(sessionsDir, { withFileTypes: true });
|
||||
} catch {
|
||||
} catch (error) {
|
||||
log(`[SessionManager] Error reading sessions directory ${sessionsDir}: ${error.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -104,7 +105,8 @@ function getSessionCandidates(options = {}) {
|
||||
let stats;
|
||||
try {
|
||||
stats = fs.statSync(sessionPath);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
log(`[SessionManager] Error stating session ${sessionPath}: ${error.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -119,8 +121,6 @@ function getSessionCandidates(options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
candidates.sort((a, b) => b.modifiedTime - a.modifiedTime);
|
||||
|
||||
const deduped = [];
|
||||
const seenFilenames = new Set();
|
||||
|
||||
@@ -132,9 +132,82 @@ function getSessionCandidates(options = {}) {
|
||||
deduped.push(session);
|
||||
}
|
||||
|
||||
deduped.sort((a, b) => b.modifiedTime - a.modifiedTime);
|
||||
return deduped;
|
||||
}
|
||||
|
||||
function buildSessionRecord(sessionPath, metadata) {
|
||||
let stats;
|
||||
try {
|
||||
stats = fs.statSync(sessionPath);
|
||||
} catch (error) {
|
||||
log(`[SessionManager] Error stating session ${sessionPath}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
sessionPath,
|
||||
hasContent: stats.size > 0,
|
||||
size: stats.size,
|
||||
modifiedTime: stats.mtime,
|
||||
createdTime: stats.birthtime || stats.ctime
|
||||
};
|
||||
}
|
||||
|
||||
function sessionMatchesId(metadata, normalizedSessionId) {
|
||||
const filename = metadata.filename;
|
||||
const shortIdMatch = metadata.shortId !== 'no-id' && metadata.shortId.startsWith(normalizedSessionId);
|
||||
const filenameMatch = filename === normalizedSessionId || filename === `${normalizedSessionId}.tmp`;
|
||||
const noIdMatch = metadata.shortId === 'no-id' && filename === `${normalizedSessionId}-session.tmp`;
|
||||
|
||||
return shortIdMatch || filenameMatch || noIdMatch;
|
||||
}
|
||||
|
||||
function getMatchingSessionCandidates(normalizedSessionId) {
|
||||
const matches = [];
|
||||
const seenFilenames = new Set();
|
||||
|
||||
for (const sessionsDir of getSessionSearchDirs()) {
|
||||
if (!fs.existsSync(sessionsDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(sessionsDir, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
log(`[SessionManager] Error reading sessions directory ${sessionsDir}: ${error.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile() || !entry.name.endsWith('.tmp')) continue;
|
||||
|
||||
const metadata = parseSessionFilename(entry.name);
|
||||
if (!metadata || !sessionMatchesId(metadata, normalizedSessionId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (seenFilenames.has(metadata.filename)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sessionPath = path.join(sessionsDir, metadata.filename);
|
||||
const sessionRecord = buildSessionRecord(sessionPath, metadata);
|
||||
if (!sessionRecord) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seenFilenames.add(metadata.filename);
|
||||
matches.push(sessionRecord);
|
||||
}
|
||||
}
|
||||
|
||||
matches.sort((a, b) => b.modifiedTime - a.modifiedTime);
|
||||
return matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse session markdown content
|
||||
* @param {string} sessionPath - Full path to session file
|
||||
@@ -331,26 +404,9 @@ function getSessionById(sessionId, includeContent = false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sessions = getSessionCandidates();
|
||||
const sessions = getMatchingSessionCandidates(normalizedSessionId);
|
||||
|
||||
for (const session of sessions) {
|
||||
const filename = session.filename;
|
||||
const metadata = {
|
||||
filename: session.filename,
|
||||
shortId: session.shortId,
|
||||
date: session.date,
|
||||
datetime: session.datetime
|
||||
};
|
||||
|
||||
// Check if session ID matches (short ID or full filename without .tmp)
|
||||
const shortIdMatch = metadata.shortId !== 'no-id' && metadata.shortId.startsWith(normalizedSessionId);
|
||||
const filenameMatch = filename === normalizedSessionId || filename === `${normalizedSessionId}.tmp`;
|
||||
const noIdMatch = metadata.shortId === 'no-id' && filename === `${normalizedSessionId}-session.tmp`;
|
||||
|
||||
if (!shortIdMatch && !filenameMatch && !noIdMatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sessionRecord = { ...session };
|
||||
|
||||
if (includeContent) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
@@ -41,6 +42,28 @@ function runHook(input, env = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function runCustomHook(pluginRoot, hookId, relScriptPath, input, env = {}) {
|
||||
const rawInput = typeof input === 'string' ? input : JSON.stringify(input);
|
||||
const result = spawnSync('node', [runner, hookId, relScriptPath, 'standard,strict'], {
|
||||
input: rawInput,
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
CLAUDE_PLUGIN_ROOT: pluginRoot,
|
||||
ECC_HOOK_PROFILE: 'standard',
|
||||
...env
|
||||
},
|
||||
timeout: 15000,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
return {
|
||||
code: Number.isInteger(result.status) ? result.status : 1,
|
||||
stdout: result.stdout || '',
|
||||
stderr: result.stderr || ''
|
||||
};
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
console.log('\n=== Testing config-protection ===\n');
|
||||
|
||||
@@ -94,6 +117,39 @@ function runTests() {
|
||||
assert.ok(result.stderr.includes('truncated payload'), `Expected truncated payload warning, got: ${result.stderr}`);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('legacy hooks do not echo raw input when they fail without stdout', () => {
|
||||
const pluginRoot = path.join(__dirname, '..', `tmp-runner-plugin-${Date.now()}`);
|
||||
const scriptDir = path.join(pluginRoot, 'scripts', 'hooks');
|
||||
const scriptPath = path.join(scriptDir, 'legacy-block.js');
|
||||
|
||||
try {
|
||||
fs.mkdirSync(scriptDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
scriptPath,
|
||||
'#!/usr/bin/env node\nprocess.stderr.write("blocked by legacy hook\\n");\nprocess.exit(2);\n'
|
||||
);
|
||||
|
||||
const rawInput = JSON.stringify({
|
||||
tool_name: 'Write',
|
||||
tool_input: {
|
||||
file_path: '.eslintrc.js',
|
||||
content: 'module.exports = {};'
|
||||
}
|
||||
});
|
||||
|
||||
const result = runCustomHook(pluginRoot, 'pre:legacy-block', 'scripts/hooks/legacy-block.js', rawInput);
|
||||
assert.strictEqual(result.code, 2, 'Expected failing legacy hook exit code to propagate');
|
||||
assert.strictEqual(result.stdout, '', 'Expected failing legacy hook to avoid raw passthrough');
|
||||
assert.ok(result.stderr.includes('blocked by legacy hook'), `Expected legacy hook stderr, got: ${result.stderr}`);
|
||||
} finally {
|
||||
try {
|
||||
fs.rmSync(pluginRoot, { recursive: true, force: true });
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
@@ -442,7 +442,7 @@ async function runTests() {
|
||||
const canonicalFile = path.join(canonicalDir, filename);
|
||||
const legacyFile = path.join(legacyDir, filename);
|
||||
const canonicalTime = new Date(now.getTime() - 60 * 1000);
|
||||
const legacyTime = new Date(now.getTime() - 120 * 1000);
|
||||
const legacyTime = new Date(canonicalTime.getTime());
|
||||
|
||||
fs.mkdirSync(canonicalDir, { recursive: true });
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
@@ -1955,12 +1955,9 @@ async function runTests() {
|
||||
assert.ok(sessionStartHook, 'Should define a SessionStart hook');
|
||||
assert.ok(sessionStartHook.command.startsWith('node -e "'), 'SessionStart should use inline node resolver');
|
||||
assert.ok(sessionStartHook.command.includes('session:start'), 'SessionStart should invoke the session:start profile');
|
||||
assert.ok(sessionStartHook.command.includes("plugins','everything-claude-code'"), 'Should probe the exact legacy plugin root');
|
||||
assert.ok(sessionStartHook.command.includes("plugins','everything-claude-code@everything-claude-code'"), 'Should probe the namespaced legacy plugin root');
|
||||
assert.ok(sessionStartHook.command.includes("plugins','marketplace','everything-claude-code'"), 'Should probe the marketplace legacy plugin root');
|
||||
assert.ok(sessionStartHook.command.includes("plugins','cache','everything-claude-code'"), 'Should retain cache lookup fallback');
|
||||
assert.ok(sessionStartHook.command.includes('if(hasRunnerRoot(envRoot))return path.resolve(envRoot.trim())'), 'Should validate CLAUDE_PLUGIN_ROOT before trusting it');
|
||||
assert.ok(sessionStartHook.command.includes('else process.stdout.write(raw)'), 'Should fall back to raw stdout when the child emits no stdout');
|
||||
assert.ok(sessionStartHook.command.includes('run-with-flags.js'), 'SessionStart should resolve the runner script');
|
||||
assert.ok(sessionStartHook.command.includes('CLAUDE_PLUGIN_ROOT'), 'SessionStart should consult CLAUDE_PLUGIN_ROOT');
|
||||
assert.ok(sessionStartHook.command.includes('plugins'), 'SessionStart should probe known plugin roots');
|
||||
assert.ok(!sessionStartHook.command.includes('find '), 'Should not scan arbitrary plugin paths with find');
|
||||
assert.ok(!sessionStartHook.command.includes('head -n 1'), 'Should not pick the first matching plugin path');
|
||||
})
|
||||
|
||||
@@ -112,14 +112,17 @@ function runTests() {
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('resolves antigravity profiles by skipping incompatible dependency trees', () => {
|
||||
if (test('resolves antigravity profiles while skipping only unsupported modules', () => {
|
||||
const projectRoot = '/workspace/app';
|
||||
const plan = resolveInstallPlan({ profileId: 'core', target: 'antigravity', projectRoot });
|
||||
|
||||
assert.deepStrictEqual(plan.selectedModuleIds, ['rules-core', 'agents-core', 'commands-core']);
|
||||
assert.deepStrictEqual(
|
||||
plan.selectedModuleIds,
|
||||
['rules-core', 'agents-core', 'commands-core', 'platform-configs', 'workflow-quality']
|
||||
);
|
||||
assert.ok(plan.skippedModuleIds.includes('hooks-runtime'));
|
||||
assert.ok(plan.skippedModuleIds.includes('platform-configs'));
|
||||
assert.ok(plan.skippedModuleIds.includes('workflow-quality'));
|
||||
assert.ok(!plan.skippedModuleIds.includes('platform-configs'));
|
||||
assert.ok(!plan.skippedModuleIds.includes('workflow-quality'));
|
||||
assert.strictEqual(plan.targetAdapterId, 'antigravity-project');
|
||||
assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.agent'));
|
||||
})) passed++; else failed++;
|
||||
|
||||
@@ -146,7 +146,7 @@ function runTests() {
|
||||
assert.strictEqual(utils.sanitizeSessionId('my-project_123'), 'my-project_123');
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('sanitizeSessionId avoids Windows reserved device names', () => {
|
||||
if (test('sanitizeSessionId appends hash suffix for all Windows reserved device names', () => {
|
||||
for (const reservedName of ['CON', 'prn', 'Aux', 'nul', 'COM1', 'lpt9']) {
|
||||
const sanitized = utils.sanitizeSessionId(reservedName);
|
||||
assert.ok(sanitized, `Expected sanitized output for ${reservedName}`);
|
||||
@@ -193,7 +193,7 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('sanitizeSessionId avoids Windows reserved device names', () => {
|
||||
if (test('sanitizeSessionId preserves readable prefixes for Windows reserved device names', () => {
|
||||
const con = utils.sanitizeSessionId('CON');
|
||||
const aux = utils.sanitizeSessionId('aux');
|
||||
assert.ok(con.startsWith('CON-'), `Expected CON to get a suffix, got: ${con}`);
|
||||
|
||||
@@ -68,9 +68,9 @@ if (
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
test('install-global-git-hooks.sh handles quoted hook paths without shell injection', () => {
|
||||
test('install-global-git-hooks.sh handles shell-sensitive hook paths without shell injection', () => {
|
||||
const homeDir = createTempDir('codex-hooks-home-');
|
||||
const weirdHooksDir = path.join(homeDir, 'git-hooks "quoted"');
|
||||
const weirdHooksDir = path.join(homeDir, "git-hooks 'quoted' & spaced");
|
||||
|
||||
try {
|
||||
const result = runBash(installScript, [], {
|
||||
|
||||
@@ -261,7 +261,7 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('installs antigravity manifest profiles while skipping incompatible modules', () => {
|
||||
if (test('installs antigravity manifest profiles while skipping only unsupported modules', () => {
|
||||
const homeDir = createTempDir('install-apply-home-');
|
||||
const projectDir = createTempDir('install-apply-project-');
|
||||
|
||||
@@ -272,14 +272,18 @@ function runTests() {
|
||||
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'rules', 'common-coding-style.md')));
|
||||
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'skills', 'architect.md')));
|
||||
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'workflows', 'plan.md')));
|
||||
assert.ok(!fs.existsSync(path.join(projectDir, '.agent', 'skills', 'tdd-workflow', 'SKILL.md')));
|
||||
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'skills', 'tdd-workflow', 'SKILL.md')));
|
||||
|
||||
const state = readJson(path.join(projectDir, '.agent', 'ecc-install-state.json'));
|
||||
assert.strictEqual(state.request.profile, 'core');
|
||||
assert.strictEqual(state.request.legacyMode, false);
|
||||
assert.deepStrictEqual(state.resolution.selectedModules, ['rules-core', 'agents-core', 'commands-core']);
|
||||
assert.ok(state.resolution.skippedModules.includes('workflow-quality'));
|
||||
assert.ok(state.resolution.skippedModules.includes('platform-configs'));
|
||||
assert.deepStrictEqual(
|
||||
state.resolution.selectedModules,
|
||||
['rules-core', 'agents-core', 'commands-core', 'platform-configs', 'workflow-quality']
|
||||
);
|
||||
assert.ok(state.resolution.skippedModules.includes('hooks-runtime'));
|
||||
assert.ok(!state.resolution.skippedModules.includes('workflow-quality'));
|
||||
assert.ok(!state.resolution.skippedModules.includes('platform-configs'));
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
cleanup(projectDir);
|
||||
|
||||
@@ -9,8 +9,32 @@ const path = require('path');
|
||||
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'sync-ecc-to-codex.sh');
|
||||
const source = fs.readFileSync(scriptPath, 'utf8');
|
||||
const normalizedSource = source.replace(/\r\n/g, '\n');
|
||||
const runOrEchoMatch = normalizedSource.match(/^run_or_echo\(\)\s*\{[\s\S]*?^}/m);
|
||||
const runOrEchoSource = runOrEchoMatch ? runOrEchoMatch[0] : '';
|
||||
const runOrEchoSource = (() => {
|
||||
const start = normalizedSource.indexOf('run_or_echo() {');
|
||||
if (start < 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let bodyStart = normalizedSource.indexOf('{', start);
|
||||
if (bodyStart < 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
for (let i = bodyStart; i < normalizedSource.length; i++) {
|
||||
const char = normalizedSource[i];
|
||||
if (char === '{') {
|
||||
depth += 1;
|
||||
} else if (char === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
return normalizedSource.slice(start, i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
})();
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user