mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 21:53:28 +08:00
Merge branch 'fix/unicode-safety-eslint-regex' into fix/harness-audit-consumer-mode
This commit is contained in:
@@ -76,9 +76,9 @@ function parseReadmeExpectations(readmeContent) {
|
||||
);
|
||||
|
||||
const tablePatterns = [
|
||||
{ category: 'agents', regex: /\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*✅\s*(\d+)\s+agents\s*\|/i, source: 'README.md comparison table' },
|
||||
{ category: 'commands', regex: /\|\s*(?:\*\*)?Commands(?:\*\*)?\s*\|\s*✅\s*(\d+)\s+commands\s*\|/i, source: 'README.md comparison table' },
|
||||
{ category: 'skills', regex: /\|\s*(?:\*\*)?Skills(?:\*\*)?\s*\|\s*✅\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) {
|
||||
|
||||
246
scripts/ci/check-unicode-safety.js
Normal file
246
scripts/ci/check-unicode-safety.js
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = process.env.ECC_UNICODE_SCAN_ROOT
|
||||
? path.resolve(process.env.ECC_UNICODE_SCAN_ROOT)
|
||||
: path.resolve(__dirname, '..', '..');
|
||||
|
||||
const writeMode = process.argv.includes('--write');
|
||||
|
||||
const ignoredDirs = new Set([
|
||||
'.git',
|
||||
'node_modules',
|
||||
'.dmux',
|
||||
'.next',
|
||||
'coverage',
|
||||
]);
|
||||
|
||||
const textExtensions = new Set([
|
||||
'.md',
|
||||
'.mdx',
|
||||
'.txt',
|
||||
'.js',
|
||||
'.cjs',
|
||||
'.mjs',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.jsx',
|
||||
'.json',
|
||||
'.toml',
|
||||
'.yml',
|
||||
'.yaml',
|
||||
'.sh',
|
||||
'.bash',
|
||||
'.zsh',
|
||||
'.ps1',
|
||||
'.py',
|
||||
'.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'),
|
||||
]);
|
||||
|
||||
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:'],
|
||||
[new RegExp(`${String.fromCodePoint(0x23ED)}(?:\\uFE0F)?`, 'gu'), 'SKIPPED:'],
|
||||
[new RegExp(String.fromCodePoint(0x2705), 'gu'), 'PASS:'],
|
||||
[new RegExp(String.fromCodePoint(0x274C), 'gu'), 'FAIL:'],
|
||||
[new RegExp(String.fromCodePoint(0x2728), 'gu'), ''],
|
||||
];
|
||||
|
||||
function shouldSkip(entryPath) {
|
||||
return entryPath.split(path.sep).some(part => ignoredDirs.has(part));
|
||||
}
|
||||
|
||||
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 })) {
|
||||
const entryPath = path.join(dirPath, entry.name);
|
||||
if (shouldSkip(entryPath)) continue;
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...listFiles(entryPath));
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && isTextFile(entryPath)) {
|
||||
results.push(entryPath);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function lineAndColumn(text, index) {
|
||||
const line = text.slice(0, index).split('\n').length;
|
||||
const lastNewline = text.lastIndexOf('\n', index - 1);
|
||||
const column = index - lastNewline;
|
||||
return { line, column };
|
||||
}
|
||||
|
||||
function isAllowedEmojiLikeSymbol(char) {
|
||||
return allowedSymbolCodePoints.has(char.codePointAt(0));
|
||||
}
|
||||
|
||||
function isDangerousInvisibleCodePoint(codePoint) {
|
||||
return (
|
||||
(codePoint >= 0x200B && codePoint <= 0x200D) ||
|
||||
codePoint === 0x2060 ||
|
||||
codePoint === 0xFEFF ||
|
||||
(codePoint >= 0x202A && codePoint <= 0x202E) ||
|
||||
(codePoint >= 0x2066 && codePoint <= 0x2069) ||
|
||||
(codePoint >= 0xFE00 && codePoint <= 0xFE0F) ||
|
||||
(codePoint >= 0xE0100 && codePoint <= 0xE01EF)
|
||||
);
|
||||
}
|
||||
|
||||
function stripDangerousInvisibleChars(text) {
|
||||
let next = '';
|
||||
for (const char of text) {
|
||||
if (!isDangerousInvisibleCodePoint(char.codePointAt(0))) {
|
||||
next += char;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function sanitizeText(text) {
|
||||
let next = text;
|
||||
next = stripDangerousInvisibleChars(next);
|
||||
|
||||
for (const [pattern, replacement] of targetedReplacements) {
|
||||
next = next.replace(pattern, replacement);
|
||||
}
|
||||
|
||||
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 ');
|
||||
next = next.replace(/^>\s{2,}/gm, '> ');
|
||||
next = next.replace(/^-\s{2,}/gm, '- ');
|
||||
next = next.replace(/^(\d+\.)\s{2,}/gm, '$1 ');
|
||||
next = next.replace(/[ \t]+$/gm, '');
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
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({
|
||||
kind,
|
||||
char,
|
||||
codePoint: `U+${char.codePointAt(0).toString(16).toUpperCase()}`,
|
||||
line,
|
||||
column,
|
||||
});
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
function collectDangerousInvisibleMatches(text) {
|
||||
const matches = [];
|
||||
let index = 0;
|
||||
|
||||
for (const char of text) {
|
||||
const codePoint = char.codePointAt(0);
|
||||
if (isDangerousInvisibleCodePoint(codePoint)) {
|
||||
const { line, column } = lineAndColumn(text, index);
|
||||
matches.push({
|
||||
kind: 'dangerous-invisible',
|
||||
char,
|
||||
codePoint: `U+${codePoint.toString(16).toUpperCase()}`,
|
||||
line,
|
||||
column,
|
||||
});
|
||||
}
|
||||
index += char.length;
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
const changedFiles = [];
|
||||
const violations = [];
|
||||
|
||||
for (const filePath of listFiles(repoRoot)) {
|
||||
const relativePath = path.relative(repoRoot, filePath);
|
||||
let text;
|
||||
try {
|
||||
text = fs.readFileSync(filePath, 'utf8');
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
writeMode &&
|
||||
!writeModeSkip.has(path.normalize(relativePath)) &&
|
||||
canAutoWrite(relativePath)
|
||||
) {
|
||||
const sanitized = sanitizeText(text);
|
||||
if (sanitized !== text) {
|
||||
fs.writeFileSync(filePath, sanitized, 'utf8');
|
||||
changedFiles.push(relativePath);
|
||||
text = sanitized;
|
||||
}
|
||||
}
|
||||
|
||||
const fileViolations = [
|
||||
...collectDangerousInvisibleMatches(text),
|
||||
...collectMatches(text, emojiRe, 'emoji'),
|
||||
];
|
||||
|
||||
for (const violation of fileViolations) {
|
||||
violations.push({
|
||||
file: relativePath,
|
||||
...violation,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (changedFiles.length > 0) {
|
||||
console.log(`Sanitized ${changedFiles.length} files:`);
|
||||
for (const file of changedFiles) {
|
||||
console.log(`- ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error('Unicode safety violations detected:');
|
||||
for (const violation of violations) {
|
||||
console.error(
|
||||
`${violation.file}:${violation.line}:${violation.column} ${violation.kind} ${violation.codePoint}`
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Unicode safety check passed.');
|
||||
@@ -306,10 +306,10 @@ function evaluate(rawInput) {
|
||||
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' ? '❌' : issue.severity === '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++;
|
||||
@@ -321,11 +321,11 @@ 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(` ⚠️ ${issue.message}`);
|
||||
console.error(` WARNING ${issue.message}`);
|
||||
if (issue.suggestion) {
|
||||
console.error(` 💡 ${issue.suggestion}`);
|
||||
console.error(` TIP ${issue.suggestion}`);
|
||||
}
|
||||
totalIssues++;
|
||||
warningCount++;
|
||||
@@ -336,21 +336,21 @@ function evaluate(rawInput) {
|
||||
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++;
|
||||
@@ -358,17 +358,17 @@ function evaluate(rawInput) {
|
||||
|
||||
// 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] ❌ 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] ⚠️ Warnings found. Consider fixing them, but commit is allowed.');
|
||||
console.error('\n[Hook] WARNING: Warnings found. Consider fixing them, but commit is allowed.');
|
||||
console.error('[Hook] To bypass these checks, use: git commit --no-verify');
|
||||
}
|
||||
} else {
|
||||
console.error('\n[Hook] ✅ All checks passed!');
|
||||
console.error('\n[Hook] PASS: All checks passed!');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -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