mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
fix: narrow unicode cleanup scope
This commit is contained in:
@@ -76,9 +76,9 @@ function parseReadmeExpectations(readmeContent) {
|
||||
);
|
||||
|
||||
const tablePatterns = [
|
||||
{ category: 'agents', regex: /\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*PASS:\s*(\d+)\s+agents\s*\|/i, source: 'README.md comparison table' },
|
||||
{ category: 'commands', regex: /\|\s*(?:\*\*)?Commands(?:\*\*)?\s*\|\s*PASS:\s*(\d+)\s+commands\s*\|/i, source: 'README.md comparison table' },
|
||||
{ category: 'skills', regex: /\|\s*(?:\*\*)?Skills(?:\*\*)?\s*\|\s*PASS:\s*(\d+)\s+skills\s*\|/i, source: 'README.md comparison table' }
|
||||
{ category: 'agents', regex: /\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s+agents\s*\|/i, source: 'README.md comparison table' },
|
||||
{ category: 'commands', regex: /\|\s*(?:\*\*)?Commands(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s+commands\s*\|/i, source: 'README.md comparison table' },
|
||||
{ category: 'skills', regex: /\|\s*(?:\*\*)?Skills(?:\*\*)?\s*\|\s*(?:(?:PASS:|\u2705)\s*)?(\d+)\s+skills\s*\|/i, source: 'README.md comparison table' }
|
||||
];
|
||||
|
||||
for (const pattern of tablePatterns) {
|
||||
|
||||
@@ -39,6 +39,12 @@ const textExtensions = new Set([
|
||||
'.rs',
|
||||
]);
|
||||
|
||||
const writableExtensions = new Set([
|
||||
'.md',
|
||||
'.mdx',
|
||||
'.txt',
|
||||
]);
|
||||
|
||||
const writeModeSkip = new Set([
|
||||
path.normalize('scripts/ci/check-unicode-safety.js'),
|
||||
path.normalize('tests/scripts/check-unicode-safety.test.js'),
|
||||
@@ -47,6 +53,11 @@ const writeModeSkip = new Set([
|
||||
const dangerousInvisibleRe =
|
||||
/[\u200B-\u200D\u2060\uFEFF\u202A-\u202E\u2066-\u2069\uFE00-\uFE0F\u{E0100}-\u{E01EF}]/gu;
|
||||
const emojiRe = /[\p{Extended_Pictographic}\p{Regional_Indicator}]/gu;
|
||||
const allowedSymbolCodePoints = new Set([
|
||||
0x00A9,
|
||||
0x00AE,
|
||||
0x2122,
|
||||
]);
|
||||
|
||||
const targetedReplacements = [
|
||||
[new RegExp(`${String.fromCodePoint(0x26A0)}(?:\\uFE0F)?`, 'gu'), 'WARNING:'],
|
||||
@@ -64,6 +75,10 @@ function isTextFile(filePath) {
|
||||
return textExtensions.has(path.extname(filePath).toLowerCase());
|
||||
}
|
||||
|
||||
function canAutoWrite(relativePath) {
|
||||
return writableExtensions.has(path.extname(relativePath).toLowerCase());
|
||||
}
|
||||
|
||||
function listFiles(dirPath) {
|
||||
const results = [];
|
||||
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
||||
@@ -87,6 +102,10 @@ function lineAndColumn(text, index) {
|
||||
return { line, column };
|
||||
}
|
||||
|
||||
function isAllowedEmojiLikeSymbol(char) {
|
||||
return allowedSymbolCodePoints.has(char.codePointAt(0));
|
||||
}
|
||||
|
||||
function sanitizeText(text) {
|
||||
let next = text;
|
||||
next = next.replace(dangerousInvisibleRe, '');
|
||||
@@ -95,7 +114,7 @@ function sanitizeText(text) {
|
||||
next = next.replace(pattern, replacement);
|
||||
}
|
||||
|
||||
next = next.replace(emojiRe, '');
|
||||
next = next.replace(emojiRe, match => (isAllowedEmojiLikeSymbol(match) ? match : ''));
|
||||
next = next.replace(/^ +(?=\*\*)/gm, '');
|
||||
next = next.replace(/^(\*\*)\s+/gm, '$1');
|
||||
next = next.replace(/^(#+)\s{2,}/gm, '$1 ');
|
||||
@@ -111,6 +130,9 @@ function collectMatches(text, regex, kind) {
|
||||
const matches = [];
|
||||
for (const match of text.matchAll(regex)) {
|
||||
const char = match[0];
|
||||
if (kind === 'emoji' && isAllowedEmojiLikeSymbol(char)) {
|
||||
continue;
|
||||
}
|
||||
const index = match.index ?? 0;
|
||||
const { line, column } = lineAndColumn(text, index);
|
||||
matches.push({
|
||||
@@ -136,7 +158,11 @@ for (const filePath of listFiles(repoRoot)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (writeMode && !writeModeSkip.has(path.normalize(relativePath))) {
|
||||
if (
|
||||
writeMode &&
|
||||
!writeModeSkip.has(path.normalize(relativePath)) &&
|
||||
canAutoWrite(relativePath)
|
||||
) {
|
||||
const sanitized = sanitizeText(text);
|
||||
if (sanitized !== text) {
|
||||
fs.writeFileSync(filePath, sanitized, 'utf8');
|
||||
|
||||
@@ -3,8 +3,8 @@ set -euo pipefail
|
||||
|
||||
# Install ECC git safety hooks globally via core.hooksPath.
|
||||
# Usage:
|
||||
# ./scripts/codex/install-global-git-hooks.sh
|
||||
# ./scripts/codex/install-global-git-hooks.sh --dry-run
|
||||
# ./scripts/codex/install-global-git-hooks.sh
|
||||
# ./scripts/codex/install-global-git-hooks.sh --dry-run
|
||||
|
||||
MODE="apply"
|
||||
if [[ "${1:-}" == "--dry-run" ]]; then
|
||||
|
||||
@@ -133,7 +133,7 @@ def write_audit(event: Dict[str, Any]) -> None:
|
||||
"""
|
||||
try:
|
||||
enriched: Dict[str, Any] = {
|
||||
**event,
|
||||
**event,
|
||||
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
}
|
||||
enriched["hash"] = hashlib.sha256(
|
||||
|
||||
@@ -49,7 +49,7 @@ function getStagedFileContent(filePath) {
|
||||
|
||||
/**
|
||||
* Check if a file should be quality-checked
|
||||
* @param {string} filePath
|
||||
* @param {string} filePath
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldCheckFile(filePath) {
|
||||
@@ -59,22 +59,22 @@ function shouldCheckFile(filePath) {
|
||||
|
||||
/**
|
||||
* Find issues in file content
|
||||
* @param {string} filePath
|
||||
* @param {string} filePath
|
||||
* @returns {object[]} Array of issues found
|
||||
*/
|
||||
function findFileIssues(filePath) {
|
||||
const issues = [];
|
||||
|
||||
|
||||
try {
|
||||
const content = getStagedFileContent(filePath);
|
||||
if (content == null) {
|
||||
return issues;
|
||||
}
|
||||
const lines = content.split('\n');
|
||||
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const lineNum = index + 1;
|
||||
|
||||
|
||||
// Check for console.log
|
||||
if (line.includes('console.log') && !line.trim().startsWith('//') && !line.trim().startsWith('*')) {
|
||||
issues.push({
|
||||
@@ -84,7 +84,7 @@ function findFileIssues(filePath) {
|
||||
severity: 'warning'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Check for debugger statements
|
||||
if (/\bdebugger\b/.test(line) && !line.trim().startsWith('//')) {
|
||||
issues.push({
|
||||
@@ -94,7 +94,7 @@ function findFileIssues(filePath) {
|
||||
severity: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Check for TODO/FIXME without issue reference
|
||||
const todoMatch = line.match(/\/\/\s*(TODO|FIXME):?\s*(.+)/);
|
||||
if (todoMatch && !todoMatch[2].match(/#\d+|issue/i)) {
|
||||
@@ -105,7 +105,7 @@ function findFileIssues(filePath) {
|
||||
severity: 'info'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Check for hardcoded secrets (basic patterns)
|
||||
const secretPatterns = [
|
||||
{ pattern: /sk-[a-zA-Z0-9]{20,}/, name: 'OpenAI API key' },
|
||||
@@ -113,7 +113,7 @@ function findFileIssues(filePath) {
|
||||
{ pattern: /AKIA[A-Z0-9]{16}/, name: 'AWS Access Key' },
|
||||
{ pattern: /api[_-]?key\s*[=:]\s*['"][^'"]+['"]/i, name: 'API key' }
|
||||
];
|
||||
|
||||
|
||||
for (const { pattern, name } of secretPatterns) {
|
||||
if (pattern.test(line)) {
|
||||
issues.push({
|
||||
@@ -128,23 +128,23 @@ function findFileIssues(filePath) {
|
||||
} catch {
|
||||
// File not readable, skip
|
||||
}
|
||||
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate commit message format
|
||||
* @param {string} command
|
||||
* @param {string} command
|
||||
* @returns {object|null} Validation result or null if no message to validate
|
||||
*/
|
||||
function validateCommitMessage(command) {
|
||||
// Extract commit message from command
|
||||
const messageMatch = command.match(/(?:-m|--message)[=\s]+["']?([^"']+)["']?/);
|
||||
if (!messageMatch) return null;
|
||||
|
||||
|
||||
const message = messageMatch[1];
|
||||
const issues = [];
|
||||
|
||||
|
||||
// Check conventional commit format
|
||||
const conventionalCommit = /^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\(.+\))?:\s*.+/;
|
||||
if (!conventionalCommit.test(message)) {
|
||||
@@ -154,7 +154,7 @@ function validateCommitMessage(command) {
|
||||
suggestion: 'Use format: type(scope): description (e.g., "feat(auth): add login flow")'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Check message length
|
||||
if (message.length > 72) {
|
||||
issues.push({
|
||||
@@ -163,7 +163,7 @@ function validateCommitMessage(command) {
|
||||
suggestion: 'Keep the first line under 72 characters'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Check for lowercase first letter (conventional)
|
||||
if (conventionalCommit.test(message)) {
|
||||
const afterColon = message.split(':')[1];
|
||||
@@ -175,7 +175,7 @@ function validateCommitMessage(command) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check for trailing period
|
||||
if (message.endsWith('.')) {
|
||||
issues.push({
|
||||
@@ -184,26 +184,26 @@ function validateCommitMessage(command) {
|
||||
suggestion: 'Remove the trailing period'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return { message, issues };
|
||||
}
|
||||
|
||||
/**
|
||||
* Run linter on staged files
|
||||
* @param {string[]} files
|
||||
* @param {string[]} files
|
||||
* @returns {object} Lint results
|
||||
*/
|
||||
function runLinter(files) {
|
||||
const jsFiles = files.filter(f => /\.(js|jsx|ts|tsx)$/.test(f));
|
||||
const pyFiles = files.filter(f => f.endsWith('.py'));
|
||||
const goFiles = files.filter(f => f.endsWith('.go'));
|
||||
|
||||
|
||||
const results = {
|
||||
eslint: null,
|
||||
pylint: null,
|
||||
golint: null
|
||||
};
|
||||
|
||||
|
||||
// Run ESLint if available
|
||||
if (jsFiles.length > 0) {
|
||||
const eslintBin = process.platform === 'win32' ? 'eslint.cmd' : 'eslint';
|
||||
@@ -220,7 +220,7 @@ function runLinter(files) {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Run Pylint if available
|
||||
if (pyFiles.length > 0) {
|
||||
try {
|
||||
@@ -241,7 +241,7 @@ function runLinter(files) {
|
||||
// Pylint not available
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Run golint if available
|
||||
if (goFiles.length > 0) {
|
||||
try {
|
||||
@@ -262,7 +262,7 @@ function runLinter(files) {
|
||||
// golint not available
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -275,41 +275,41 @@ function evaluate(rawInput) {
|
||||
try {
|
||||
const input = JSON.parse(rawInput);
|
||||
const command = input.tool_input?.command || '';
|
||||
|
||||
|
||||
// Only run for git commit commands
|
||||
if (!command.includes('git commit')) {
|
||||
return { output: rawInput, exitCode: 0 };
|
||||
}
|
||||
|
||||
|
||||
// Check if this is an amend (skip checks for amends to avoid blocking)
|
||||
if (command.includes('--amend')) {
|
||||
return { output: rawInput, exitCode: 0 };
|
||||
}
|
||||
|
||||
|
||||
// Get staged files
|
||||
const stagedFiles = getStagedFiles();
|
||||
|
||||
|
||||
if (stagedFiles.length === 0) {
|
||||
console.error('[Hook] No staged files found. Use "git add" to stage files first.');
|
||||
return { output: rawInput, exitCode: 0 };
|
||||
}
|
||||
|
||||
|
||||
console.error(`[Hook] Checking ${stagedFiles.length} staged file(s)...`);
|
||||
|
||||
|
||||
// Check each staged file
|
||||
const filesToCheck = stagedFiles.filter(shouldCheckFile);
|
||||
let totalIssues = 0;
|
||||
let errorCount = 0;
|
||||
let warningCount = 0;
|
||||
let infoCount = 0;
|
||||
|
||||
|
||||
for (const file of filesToCheck) {
|
||||
const fileIssues = findFileIssues(file);
|
||||
if (fileIssues.length > 0) {
|
||||
console.error(`\n ${file}`);
|
||||
console.error(`\n[FILE] ${file}`);
|
||||
for (const issue of fileIssues) {
|
||||
const icon = issue.severity === 'error' ? 'FAIL:' : issue.severity === 'warning' ? 'WARNING:' : '';
|
||||
console.error(` ${icon} Line ${issue.line}: ${issue.message}`);
|
||||
const label = issue.severity === 'error' ? 'ERROR' : issue.severity === 'warning' ? 'WARNING' : 'INFO';
|
||||
console.error(` ${label} Line ${issue.line}: ${issue.message}`);
|
||||
totalIssues++;
|
||||
if (issue.severity === 'error') errorCount++;
|
||||
if (issue.severity === 'warning') warningCount++;
|
||||
@@ -317,51 +317,51 @@ function evaluate(rawInput) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Validate commit message if provided
|
||||
const messageValidation = validateCommitMessage(command);
|
||||
if (messageValidation && messageValidation.issues.length > 0) {
|
||||
console.error('\n Commit Message Issues:');
|
||||
console.error('\nCommit Message Issues:');
|
||||
for (const issue of messageValidation.issues) {
|
||||
console.error(` WARNING: ${issue.message}`);
|
||||
console.error(` WARNING ${issue.message}`);
|
||||
if (issue.suggestion) {
|
||||
console.error(` ${issue.suggestion}`);
|
||||
console.error(` TIP ${issue.suggestion}`);
|
||||
}
|
||||
totalIssues++;
|
||||
warningCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Run linter
|
||||
const lintResults = runLinter(filesToCheck);
|
||||
|
||||
|
||||
if (lintResults.eslint && !lintResults.eslint.success) {
|
||||
console.error('\n ESLint Issues:');
|
||||
console.error('\nESLint Issues:');
|
||||
console.error(lintResults.eslint.output);
|
||||
totalIssues++;
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
|
||||
if (lintResults.pylint && !lintResults.pylint.success) {
|
||||
console.error('\n Pylint Issues:');
|
||||
console.error('\nPylint Issues:');
|
||||
console.error(lintResults.pylint.output);
|
||||
totalIssues++;
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
|
||||
if (lintResults.golint && !lintResults.golint.success) {
|
||||
console.error('\n golint Issues:');
|
||||
console.error('\ngolint Issues:');
|
||||
console.error(lintResults.golint.output);
|
||||
totalIssues++;
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
|
||||
// Summary
|
||||
if (totalIssues > 0) {
|
||||
console.error(`\n Summary: ${totalIssues} issue(s) found (${errorCount} error(s), ${warningCount} warning(s), ${infoCount} info)`);
|
||||
|
||||
console.error(`\nSummary: ${totalIssues} issue(s) found (${errorCount} error(s), ${warningCount} warning(s), ${infoCount} info)`);
|
||||
|
||||
if (errorCount > 0) {
|
||||
console.error('\n[Hook] FAIL: Commit blocked due to critical issues. Fix them before committing.');
|
||||
console.error('\n[Hook] ERROR: Commit blocked due to critical issues. Fix them before committing.');
|
||||
return { output: rawInput, exitCode: 2 };
|
||||
} else {
|
||||
console.error('\n[Hook] WARNING: Warnings found. Consider fixing them, but commit is allowed.');
|
||||
@@ -370,12 +370,12 @@ function evaluate(rawInput) {
|
||||
} else {
|
||||
console.error('\n[Hook] PASS: All checks passed!');
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[Hook] Error: ${error.message}`);
|
||||
// Non-blocking on error
|
||||
}
|
||||
|
||||
|
||||
return { output: rawInput, exitCode: 0 };
|
||||
}
|
||||
|
||||
@@ -387,14 +387,14 @@ function run(rawInput) {
|
||||
if (require.main === module) {
|
||||
let data = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
|
||||
process.stdin.on('data', chunk => {
|
||||
if (data.length < MAX_STDIN) {
|
||||
const remaining = MAX_STDIN - data.length;
|
||||
data += chunk.substring(0, remaining);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
const result = evaluate(data);
|
||||
process.stdout.write(result.output);
|
||||
|
||||
@@ -64,7 +64,7 @@ function sleep(ms) {
|
||||
}
|
||||
|
||||
async function animateProgress(label, steps, callback) {
|
||||
process.stdout.write(`\n${chalk.cyan('')} ${label}...\n`);
|
||||
process.stdout.write(`\n${chalk.cyan('[RUN]')} ${label}...\n`);
|
||||
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const step = steps[i];
|
||||
@@ -72,7 +72,7 @@ async function animateProgress(label, steps, callback) {
|
||||
await sleep(step.duration || 500);
|
||||
process.stdout.clearLine?.(0) || process.stdout.write('\r');
|
||||
process.stdout.cursorTo?.(0) || process.stdout.write('\r');
|
||||
process.stdout.write(` ${chalk.green('✓')} ${step.name}\n`);
|
||||
process.stdout.write(` ${chalk.green('[DONE]')} ${step.name}\n`);
|
||||
if (callback) callback(step, i);
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ class SkillCreateOutput {
|
||||
|
||||
console.log('\n');
|
||||
console.log(chalk.bold(chalk.magenta('╔════════════════════════════════════════════════════════════════╗')));
|
||||
console.log(chalk.bold(chalk.magenta('║')) + chalk.bold(' ECC Skill Creator ') + chalk.bold(chalk.magenta('║')));
|
||||
console.log(chalk.bold(chalk.magenta('║')) + chalk.bold(' ECC Skill Creator ') + chalk.bold(chalk.magenta('║')));
|
||||
console.log(chalk.bold(chalk.magenta('║')) + ` ${subtitle}${' '.repeat(Math.max(0, 59 - stripAnsi(subtitle).length))}` + chalk.bold(chalk.magenta('║')));
|
||||
console.log(chalk.bold(chalk.magenta('╚════════════════════════════════════════════════════════════════╝')));
|
||||
console.log('');
|
||||
@@ -111,7 +111,7 @@ class SkillCreateOutput {
|
||||
|
||||
analysisResults(data) {
|
||||
console.log('\n');
|
||||
console.log(box(' Analysis Results', `
|
||||
console.log(box('Analysis Results', `
|
||||
${chalk.bold('Commits Analyzed:')} ${chalk.yellow(data.commits)}
|
||||
${chalk.bold('Time Range:')} ${chalk.gray(data.timeRange)}
|
||||
${chalk.bold('Contributors:')} ${chalk.cyan(data.contributors)}
|
||||
@@ -121,7 +121,7 @@ ${chalk.bold('Files Tracked:')} ${chalk.green(data.files)}
|
||||
|
||||
patterns(patterns) {
|
||||
console.log('\n');
|
||||
console.log(chalk.bold(chalk.cyan(' Key Patterns Discovered:')));
|
||||
console.log(chalk.bold(chalk.cyan('Key Patterns Discovered:')));
|
||||
console.log(chalk.gray('─'.repeat(50)));
|
||||
|
||||
patterns.forEach((pattern, i) => {
|
||||
@@ -137,26 +137,26 @@ ${chalk.bold('Files Tracked:')} ${chalk.green(data.files)}
|
||||
|
||||
instincts(instincts) {
|
||||
console.log('\n');
|
||||
console.log(box(' Instincts Generated', instincts.map((inst, i) =>
|
||||
console.log(box('Instincts Generated', instincts.map((inst, i) =>
|
||||
`${chalk.yellow(`${i + 1}.`)} ${chalk.bold(inst.name)} ${chalk.gray(`(${Math.round(inst.confidence * 100)}%)`)}`
|
||||
).join('\n')));
|
||||
}
|
||||
|
||||
output(skillPath, instinctsPath) {
|
||||
console.log('\n');
|
||||
console.log(chalk.bold(chalk.green(' Generation Complete!')));
|
||||
console.log(chalk.bold(chalk.green('Generation Complete!')));
|
||||
console.log(chalk.gray('─'.repeat(50)));
|
||||
console.log(`
|
||||
${chalk.green('')} ${chalk.bold('Skill File:')}
|
||||
${chalk.green('-')} ${chalk.bold('Skill File:')}
|
||||
${chalk.cyan(skillPath)}
|
||||
|
||||
${chalk.green('')} ${chalk.bold('Instincts File:')}
|
||||
${chalk.green('-')} ${chalk.bold('Instincts File:')}
|
||||
${chalk.cyan(instinctsPath)}
|
||||
`);
|
||||
}
|
||||
|
||||
nextSteps() {
|
||||
console.log(box(' Next Steps', `
|
||||
console.log(box('Next Steps', `
|
||||
${chalk.yellow('1.')} Review the generated SKILL.md
|
||||
${chalk.yellow('2.')} Import instincts: ${chalk.cyan('/instinct-import <path>')}
|
||||
${chalk.yellow('3.')} View learned patterns: ${chalk.cyan('/instinct-status')}
|
||||
|
||||
Reference in New Issue
Block a user