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:
@@ -4,10 +4,12 @@
|
|||||||
# Installs Everything Claude Code workflows into a Kiro project.
|
# Installs Everything Claude Code workflows into a Kiro project.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./install.sh # Install to current directory
|
# ./install.sh # Install to current directory
|
||||||
# ./install.sh /path/to/dir # Install to specific directory
|
# ./install.sh /path/to/dir # Install to specific directory
|
||||||
# ./install.sh ~ # Install globally to ~/.kiro/
|
# ./install.sh ~ # Install globally to ~/.kiro/
|
||||||
# set -euo pipefail
|
#
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
# When globs match nothing, expand to empty list instead of the literal pattern
|
# When globs match nothing, expand to empty list instead of the literal pattern
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ case "$FORMATTER" in
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
prettier)
|
prettier)
|
||||||
if command -v npx &>/dev/null; then
|
if command -v npx &>/dev/null; then
|
||||||
echo "Formatting $FILE with Prettier..."
|
echo "Formatting $FILE with Prettier..."
|
||||||
@@ -61,7 +61,7 @@ case "$FORMATTER" in
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
none)
|
none)
|
||||||
echo "No formatter detected (biome.json, .prettierrc, or installed formatter)"
|
echo "No formatter detected (biome.json, .prettierrc, or installed formatter)"
|
||||||
echo "Skipping format for: $FILE"
|
echo "Skipping format for: $FILE"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ detect_pm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PM=$(detect_pm)
|
PM=$(detect_pm)
|
||||||
echo " Package manager: $PM"
|
echo "Package manager: $PM"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ── Helper: run a check ─────────────────────────────────────
|
# ── Helper: run a check ─────────────────────────────────────
|
||||||
|
|||||||
@@ -4,12 +4,14 @@
|
|||||||
# Installs Everything Claude Code workflows into a Trae project.
|
# Installs Everything Claude Code workflows into a Trae project.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./install.sh # Install to current directory
|
# ./install.sh # Install to current directory
|
||||||
# ./install.sh ~ # Install globally to ~/.trae/ or ~/.trae-cn/
|
# ./install.sh ~ # Install globally to ~/.trae/ or ~/.trae-cn/
|
||||||
#
|
#
|
||||||
# Environment:
|
# Environment:
|
||||||
# TRAE_ENV=cn # Force use .trae-cn directory
|
# TRAE_ENV=cn # Force use .trae-cn directory
|
||||||
# set -euo pipefail
|
#
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
# When globs match nothing, expand to empty list instead of the literal pattern
|
# When globs match nothing, expand to empty list instead of the literal pattern
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
|
|||||||
@@ -4,12 +4,14 @@
|
|||||||
# Uninstalls Everything Claude Code workflows from a Trae project.
|
# Uninstalls Everything Claude Code workflows from a Trae project.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./uninstall.sh # Uninstall from current directory
|
# ./uninstall.sh # Uninstall from current directory
|
||||||
# ./uninstall.sh ~ # Uninstall globally from ~/.trae/
|
# ./uninstall.sh ~ # Uninstall globally from ~/.trae/
|
||||||
#
|
#
|
||||||
# Environment:
|
# Environment:
|
||||||
# TRAE_ENV=cn # Force use .trae-cn directory
|
# TRAE_ENV=cn # Force use .trae-cn directory
|
||||||
# set -euo pipefail
|
#
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
# Resolve the directory where this script lives
|
# Resolve the directory where this script lives
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
@@ -44,18 +46,18 @@ is_valid_manifest_entry() {
|
|||||||
do_uninstall() {
|
do_uninstall() {
|
||||||
local target_dir="$PWD"
|
local target_dir="$PWD"
|
||||||
local trae_dir="$(get_trae_dir)"
|
local trae_dir="$(get_trae_dir)"
|
||||||
|
|
||||||
# Check if ~ was specified (or expanded to $HOME)
|
# Check if ~ was specified (or expanded to $HOME)
|
||||||
if [ "$#" -ge 1 ]; then
|
if [ "$#" -ge 1 ]; then
|
||||||
if [ "$1" = "~" ] || [ "$1" = "$HOME" ]; then
|
if [ "$1" = "~" ] || [ "$1" = "$HOME" ]; then
|
||||||
target_dir="$HOME"
|
target_dir="$HOME"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if we're already inside a .trae or .trae-cn directory
|
# Check if we're already inside a .trae or .trae-cn directory
|
||||||
local current_dir_name="$(basename "$target_dir")"
|
local current_dir_name="$(basename "$target_dir")"
|
||||||
local trae_full_path
|
local trae_full_path
|
||||||
|
|
||||||
if [ "$current_dir_name" = ".trae" ] || [ "$current_dir_name" = ".trae-cn" ]; then
|
if [ "$current_dir_name" = ".trae" ] || [ "$current_dir_name" = ".trae-cn" ]; then
|
||||||
# Already inside the trae directory, use it directly
|
# Already inside the trae directory, use it directly
|
||||||
trae_full_path="$target_dir"
|
trae_full_path="$target_dir"
|
||||||
@@ -63,23 +65,23 @@ do_uninstall() {
|
|||||||
# Normal case: append trae_dir to target_dir
|
# Normal case: append trae_dir to target_dir
|
||||||
trae_full_path="$target_dir/$trae_dir"
|
trae_full_path="$target_dir/$trae_dir"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "ECC Trae Uninstaller"
|
echo "ECC Trae Uninstaller"
|
||||||
echo "===================="
|
echo "===================="
|
||||||
echo ""
|
echo ""
|
||||||
echo "Target: $trae_full_path/"
|
echo "Target: $trae_full_path/"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [ ! -d "$trae_full_path" ]; then
|
if [ ! -d "$trae_full_path" ]; then
|
||||||
echo "Error: $trae_dir directory not found at $target_dir"
|
echo "Error: $trae_dir directory not found at $target_dir"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
trae_root_resolved="$(resolve_path "$trae_full_path")"
|
trae_root_resolved="$(resolve_path "$trae_full_path")"
|
||||||
|
|
||||||
# Manifest file path
|
# Manifest file path
|
||||||
MANIFEST="$trae_full_path/.ecc-manifest"
|
MANIFEST="$trae_full_path/.ecc-manifest"
|
||||||
|
|
||||||
if [ ! -f "$MANIFEST" ]; then
|
if [ ! -f "$MANIFEST" ]; then
|
||||||
echo "Warning: No manifest file found (.ecc-manifest)"
|
echo "Warning: No manifest file found (.ecc-manifest)"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -99,7 +101,7 @@ do_uninstall() {
|
|||||||
echo "Removed: $trae_full_path/"
|
echo "Removed: $trae_full_path/"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Found manifest file - will only remove files installed by ECC"
|
echo "Found manifest file - will only remove files installed by ECC"
|
||||||
echo ""
|
echo ""
|
||||||
read -p "Are you sure you want to uninstall ECC from $trae_dir? (y/N) " -n 1 -r
|
read -p "Are you sure you want to uninstall ECC from $trae_dir? (y/N) " -n 1 -r
|
||||||
@@ -108,11 +110,11 @@ do_uninstall() {
|
|||||||
echo "Uninstall cancelled."
|
echo "Uninstall cancelled."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Counters
|
# Counters
|
||||||
removed=0
|
removed=0
|
||||||
skipped=0
|
skipped=0
|
||||||
|
|
||||||
# Read manifest and remove files
|
# Read manifest and remove files
|
||||||
while IFS= read -r file_path; do
|
while IFS= read -r file_path; do
|
||||||
[ -z "$file_path" ] && continue
|
[ -z "$file_path" ] && continue
|
||||||
@@ -166,7 +168,7 @@ do_uninstall() {
|
|||||||
removed=$((removed + 1))
|
removed=$((removed + 1))
|
||||||
fi
|
fi
|
||||||
done < <(find "$trae_full_path" -depth -type d -empty 2>/dev/null | sort -r)
|
done < <(find "$trae_full_path" -depth -type d -empty 2>/dev/null | sort -r)
|
||||||
|
|
||||||
# Try to remove the main trae directory if it's empty
|
# Try to remove the main trae directory if it's empty
|
||||||
if [ -d "$trae_full_path" ] && [ -z "$(ls -A "$trae_full_path" 2>/dev/null)" ]; then
|
if [ -d "$trae_full_path" ] && [ -z "$(ls -A "$trae_full_path" 2>/dev/null)" ]; then
|
||||||
rmdir "$trae_full_path" 2>/dev/null || true
|
rmdir "$trae_full_path" 2>/dev/null || true
|
||||||
@@ -175,7 +177,7 @@ do_uninstall() {
|
|||||||
removed=$((removed + 1))
|
removed=$((removed + 1))
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Uninstall complete!"
|
echo "Uninstall complete!"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ function parseReadmeExpectations(readmeContent) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const tablePatterns = [
|
const tablePatterns = [
|
||||||
{ category: 'agents', regex: /\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*PASS:\s*(\d+)\s+agents\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:\s*(\d+)\s+commands\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:\s*(\d+)\s+skills\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) {
|
for (const pattern of tablePatterns) {
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ const textExtensions = new Set([
|
|||||||
'.rs',
|
'.rs',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const writableExtensions = new Set([
|
||||||
|
'.md',
|
||||||
|
'.mdx',
|
||||||
|
'.txt',
|
||||||
|
]);
|
||||||
|
|
||||||
const writeModeSkip = new Set([
|
const writeModeSkip = new Set([
|
||||||
path.normalize('scripts/ci/check-unicode-safety.js'),
|
path.normalize('scripts/ci/check-unicode-safety.js'),
|
||||||
path.normalize('tests/scripts/check-unicode-safety.test.js'),
|
path.normalize('tests/scripts/check-unicode-safety.test.js'),
|
||||||
@@ -47,6 +53,11 @@ const writeModeSkip = new Set([
|
|||||||
const dangerousInvisibleRe =
|
const dangerousInvisibleRe =
|
||||||
/[\u200B-\u200D\u2060\uFEFF\u202A-\u202E\u2066-\u2069\uFE00-\uFE0F\u{E0100}-\u{E01EF}]/gu;
|
/[\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 emojiRe = /[\p{Extended_Pictographic}\p{Regional_Indicator}]/gu;
|
||||||
|
const allowedSymbolCodePoints = new Set([
|
||||||
|
0x00A9,
|
||||||
|
0x00AE,
|
||||||
|
0x2122,
|
||||||
|
]);
|
||||||
|
|
||||||
const targetedReplacements = [
|
const targetedReplacements = [
|
||||||
[new RegExp(`${String.fromCodePoint(0x26A0)}(?:\\uFE0F)?`, 'gu'), 'WARNING:'],
|
[new RegExp(`${String.fromCodePoint(0x26A0)}(?:\\uFE0F)?`, 'gu'), 'WARNING:'],
|
||||||
@@ -64,6 +75,10 @@ function isTextFile(filePath) {
|
|||||||
return textExtensions.has(path.extname(filePath).toLowerCase());
|
return textExtensions.has(path.extname(filePath).toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canAutoWrite(relativePath) {
|
||||||
|
return writableExtensions.has(path.extname(relativePath).toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
function listFiles(dirPath) {
|
function listFiles(dirPath) {
|
||||||
const results = [];
|
const results = [];
|
||||||
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
||||||
@@ -87,6 +102,10 @@ function lineAndColumn(text, index) {
|
|||||||
return { line, column };
|
return { line, column };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAllowedEmojiLikeSymbol(char) {
|
||||||
|
return allowedSymbolCodePoints.has(char.codePointAt(0));
|
||||||
|
}
|
||||||
|
|
||||||
function sanitizeText(text) {
|
function sanitizeText(text) {
|
||||||
let next = text;
|
let next = text;
|
||||||
next = next.replace(dangerousInvisibleRe, '');
|
next = next.replace(dangerousInvisibleRe, '');
|
||||||
@@ -95,7 +114,7 @@ function sanitizeText(text) {
|
|||||||
next = next.replace(pattern, replacement);
|
next = next.replace(pattern, replacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
next = next.replace(emojiRe, '');
|
next = next.replace(emojiRe, match => (isAllowedEmojiLikeSymbol(match) ? match : ''));
|
||||||
next = next.replace(/^ +(?=\*\*)/gm, '');
|
next = next.replace(/^ +(?=\*\*)/gm, '');
|
||||||
next = next.replace(/^(\*\*)\s+/gm, '$1');
|
next = next.replace(/^(\*\*)\s+/gm, '$1');
|
||||||
next = next.replace(/^(#+)\s{2,}/gm, '$1 ');
|
next = next.replace(/^(#+)\s{2,}/gm, '$1 ');
|
||||||
@@ -111,6 +130,9 @@ function collectMatches(text, regex, kind) {
|
|||||||
const matches = [];
|
const matches = [];
|
||||||
for (const match of text.matchAll(regex)) {
|
for (const match of text.matchAll(regex)) {
|
||||||
const char = match[0];
|
const char = match[0];
|
||||||
|
if (kind === 'emoji' && isAllowedEmojiLikeSymbol(char)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const index = match.index ?? 0;
|
const index = match.index ?? 0;
|
||||||
const { line, column } = lineAndColumn(text, index);
|
const { line, column } = lineAndColumn(text, index);
|
||||||
matches.push({
|
matches.push({
|
||||||
@@ -136,7 +158,11 @@ for (const filePath of listFiles(repoRoot)) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (writeMode && !writeModeSkip.has(path.normalize(relativePath))) {
|
if (
|
||||||
|
writeMode &&
|
||||||
|
!writeModeSkip.has(path.normalize(relativePath)) &&
|
||||||
|
canAutoWrite(relativePath)
|
||||||
|
) {
|
||||||
const sanitized = sanitizeText(text);
|
const sanitized = sanitizeText(text);
|
||||||
if (sanitized !== text) {
|
if (sanitized !== text) {
|
||||||
fs.writeFileSync(filePath, sanitized, 'utf8');
|
fs.writeFileSync(filePath, sanitized, 'utf8');
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ set -euo pipefail
|
|||||||
|
|
||||||
# Install ECC git safety hooks globally via core.hooksPath.
|
# Install ECC git safety hooks globally via core.hooksPath.
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./scripts/codex/install-global-git-hooks.sh
|
# ./scripts/codex/install-global-git-hooks.sh
|
||||||
# ./scripts/codex/install-global-git-hooks.sh --dry-run
|
# ./scripts/codex/install-global-git-hooks.sh --dry-run
|
||||||
|
|
||||||
MODE="apply"
|
MODE="apply"
|
||||||
if [[ "${1:-}" == "--dry-run" ]]; then
|
if [[ "${1:-}" == "--dry-run" ]]; then
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ def write_audit(event: Dict[str, Any]) -> None:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
enriched: Dict[str, Any] = {
|
enriched: Dict[str, Any] = {
|
||||||
**event,
|
**event,
|
||||||
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
}
|
}
|
||||||
enriched["hash"] = hashlib.sha256(
|
enriched["hash"] = hashlib.sha256(
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function getStagedFileContent(filePath) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a file should be quality-checked
|
* Check if a file should be quality-checked
|
||||||
* @param {string} filePath
|
* @param {string} filePath
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
function shouldCheckFile(filePath) {
|
function shouldCheckFile(filePath) {
|
||||||
@@ -59,22 +59,22 @@ function shouldCheckFile(filePath) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Find issues in file content
|
* Find issues in file content
|
||||||
* @param {string} filePath
|
* @param {string} filePath
|
||||||
* @returns {object[]} Array of issues found
|
* @returns {object[]} Array of issues found
|
||||||
*/
|
*/
|
||||||
function findFileIssues(filePath) {
|
function findFileIssues(filePath) {
|
||||||
const issues = [];
|
const issues = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = getStagedFileContent(filePath);
|
const content = getStagedFileContent(filePath);
|
||||||
if (content == null) {
|
if (content == null) {
|
||||||
return issues;
|
return issues;
|
||||||
}
|
}
|
||||||
const lines = content.split('\n');
|
const lines = content.split('\n');
|
||||||
|
|
||||||
lines.forEach((line, index) => {
|
lines.forEach((line, index) => {
|
||||||
const lineNum = index + 1;
|
const lineNum = index + 1;
|
||||||
|
|
||||||
// Check for console.log
|
// Check for console.log
|
||||||
if (line.includes('console.log') && !line.trim().startsWith('//') && !line.trim().startsWith('*')) {
|
if (line.includes('console.log') && !line.trim().startsWith('//') && !line.trim().startsWith('*')) {
|
||||||
issues.push({
|
issues.push({
|
||||||
@@ -84,7 +84,7 @@ function findFileIssues(filePath) {
|
|||||||
severity: 'warning'
|
severity: 'warning'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for debugger statements
|
// Check for debugger statements
|
||||||
if (/\bdebugger\b/.test(line) && !line.trim().startsWith('//')) {
|
if (/\bdebugger\b/.test(line) && !line.trim().startsWith('//')) {
|
||||||
issues.push({
|
issues.push({
|
||||||
@@ -94,7 +94,7 @@ function findFileIssues(filePath) {
|
|||||||
severity: 'error'
|
severity: 'error'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for TODO/FIXME without issue reference
|
// Check for TODO/FIXME without issue reference
|
||||||
const todoMatch = line.match(/\/\/\s*(TODO|FIXME):?\s*(.+)/);
|
const todoMatch = line.match(/\/\/\s*(TODO|FIXME):?\s*(.+)/);
|
||||||
if (todoMatch && !todoMatch[2].match(/#\d+|issue/i)) {
|
if (todoMatch && !todoMatch[2].match(/#\d+|issue/i)) {
|
||||||
@@ -105,7 +105,7 @@ function findFileIssues(filePath) {
|
|||||||
severity: 'info'
|
severity: 'info'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for hardcoded secrets (basic patterns)
|
// Check for hardcoded secrets (basic patterns)
|
||||||
const secretPatterns = [
|
const secretPatterns = [
|
||||||
{ pattern: /sk-[a-zA-Z0-9]{20,}/, name: 'OpenAI API key' },
|
{ 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: /AKIA[A-Z0-9]{16}/, name: 'AWS Access Key' },
|
||||||
{ pattern: /api[_-]?key\s*[=:]\s*['"][^'"]+['"]/i, name: 'API key' }
|
{ pattern: /api[_-]?key\s*[=:]\s*['"][^'"]+['"]/i, name: 'API key' }
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const { pattern, name } of secretPatterns) {
|
for (const { pattern, name } of secretPatterns) {
|
||||||
if (pattern.test(line)) {
|
if (pattern.test(line)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
@@ -128,23 +128,23 @@ function findFileIssues(filePath) {
|
|||||||
} catch {
|
} catch {
|
||||||
// File not readable, skip
|
// File not readable, skip
|
||||||
}
|
}
|
||||||
|
|
||||||
return issues;
|
return issues;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate commit message format
|
* Validate commit message format
|
||||||
* @param {string} command
|
* @param {string} command
|
||||||
* @returns {object|null} Validation result or null if no message to validate
|
* @returns {object|null} Validation result or null if no message to validate
|
||||||
*/
|
*/
|
||||||
function validateCommitMessage(command) {
|
function validateCommitMessage(command) {
|
||||||
// Extract commit message from command
|
// Extract commit message from command
|
||||||
const messageMatch = command.match(/(?:-m|--message)[=\s]+["']?([^"']+)["']?/);
|
const messageMatch = command.match(/(?:-m|--message)[=\s]+["']?([^"']+)["']?/);
|
||||||
if (!messageMatch) return null;
|
if (!messageMatch) return null;
|
||||||
|
|
||||||
const message = messageMatch[1];
|
const message = messageMatch[1];
|
||||||
const issues = [];
|
const issues = [];
|
||||||
|
|
||||||
// Check conventional commit format
|
// Check conventional commit format
|
||||||
const conventionalCommit = /^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\(.+\))?:\s*.+/;
|
const conventionalCommit = /^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\(.+\))?:\s*.+/;
|
||||||
if (!conventionalCommit.test(message)) {
|
if (!conventionalCommit.test(message)) {
|
||||||
@@ -154,7 +154,7 @@ function validateCommitMessage(command) {
|
|||||||
suggestion: 'Use format: type(scope): description (e.g., "feat(auth): add login flow")'
|
suggestion: 'Use format: type(scope): description (e.g., "feat(auth): add login flow")'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check message length
|
// Check message length
|
||||||
if (message.length > 72) {
|
if (message.length > 72) {
|
||||||
issues.push({
|
issues.push({
|
||||||
@@ -163,7 +163,7 @@ function validateCommitMessage(command) {
|
|||||||
suggestion: 'Keep the first line under 72 characters'
|
suggestion: 'Keep the first line under 72 characters'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for lowercase first letter (conventional)
|
// Check for lowercase first letter (conventional)
|
||||||
if (conventionalCommit.test(message)) {
|
if (conventionalCommit.test(message)) {
|
||||||
const afterColon = message.split(':')[1];
|
const afterColon = message.split(':')[1];
|
||||||
@@ -175,7 +175,7 @@ function validateCommitMessage(command) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for trailing period
|
// Check for trailing period
|
||||||
if (message.endsWith('.')) {
|
if (message.endsWith('.')) {
|
||||||
issues.push({
|
issues.push({
|
||||||
@@ -184,26 +184,26 @@ function validateCommitMessage(command) {
|
|||||||
suggestion: 'Remove the trailing period'
|
suggestion: 'Remove the trailing period'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { message, issues };
|
return { message, issues };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run linter on staged files
|
* Run linter on staged files
|
||||||
* @param {string[]} files
|
* @param {string[]} files
|
||||||
* @returns {object} Lint results
|
* @returns {object} Lint results
|
||||||
*/
|
*/
|
||||||
function runLinter(files) {
|
function runLinter(files) {
|
||||||
const jsFiles = files.filter(f => /\.(js|jsx|ts|tsx)$/.test(f));
|
const jsFiles = files.filter(f => /\.(js|jsx|ts|tsx)$/.test(f));
|
||||||
const pyFiles = files.filter(f => f.endsWith('.py'));
|
const pyFiles = files.filter(f => f.endsWith('.py'));
|
||||||
const goFiles = files.filter(f => f.endsWith('.go'));
|
const goFiles = files.filter(f => f.endsWith('.go'));
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
eslint: null,
|
eslint: null,
|
||||||
pylint: null,
|
pylint: null,
|
||||||
golint: null
|
golint: null
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run ESLint if available
|
// Run ESLint if available
|
||||||
if (jsFiles.length > 0) {
|
if (jsFiles.length > 0) {
|
||||||
const eslintBin = process.platform === 'win32' ? 'eslint.cmd' : 'eslint';
|
const eslintBin = process.platform === 'win32' ? 'eslint.cmd' : 'eslint';
|
||||||
@@ -220,7 +220,7 @@ function runLinter(files) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run Pylint if available
|
// Run Pylint if available
|
||||||
if (pyFiles.length > 0) {
|
if (pyFiles.length > 0) {
|
||||||
try {
|
try {
|
||||||
@@ -241,7 +241,7 @@ function runLinter(files) {
|
|||||||
// Pylint not available
|
// Pylint not available
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run golint if available
|
// Run golint if available
|
||||||
if (goFiles.length > 0) {
|
if (goFiles.length > 0) {
|
||||||
try {
|
try {
|
||||||
@@ -262,7 +262,7 @@ function runLinter(files) {
|
|||||||
// golint not available
|
// golint not available
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,41 +275,41 @@ function evaluate(rawInput) {
|
|||||||
try {
|
try {
|
||||||
const input = JSON.parse(rawInput);
|
const input = JSON.parse(rawInput);
|
||||||
const command = input.tool_input?.command || '';
|
const command = input.tool_input?.command || '';
|
||||||
|
|
||||||
// Only run for git commit commands
|
// Only run for git commit commands
|
||||||
if (!command.includes('git commit')) {
|
if (!command.includes('git commit')) {
|
||||||
return { output: rawInput, exitCode: 0 };
|
return { output: rawInput, exitCode: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is an amend (skip checks for amends to avoid blocking)
|
// Check if this is an amend (skip checks for amends to avoid blocking)
|
||||||
if (command.includes('--amend')) {
|
if (command.includes('--amend')) {
|
||||||
return { output: rawInput, exitCode: 0 };
|
return { output: rawInput, exitCode: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get staged files
|
// Get staged files
|
||||||
const stagedFiles = getStagedFiles();
|
const stagedFiles = getStagedFiles();
|
||||||
|
|
||||||
if (stagedFiles.length === 0) {
|
if (stagedFiles.length === 0) {
|
||||||
console.error('[Hook] No staged files found. Use "git add" to stage files first.');
|
console.error('[Hook] No staged files found. Use "git add" to stage files first.');
|
||||||
return { output: rawInput, exitCode: 0 };
|
return { output: rawInput, exitCode: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`[Hook] Checking ${stagedFiles.length} staged file(s)...`);
|
console.error(`[Hook] Checking ${stagedFiles.length} staged file(s)...`);
|
||||||
|
|
||||||
// Check each staged file
|
// Check each staged file
|
||||||
const filesToCheck = stagedFiles.filter(shouldCheckFile);
|
const filesToCheck = stagedFiles.filter(shouldCheckFile);
|
||||||
let totalIssues = 0;
|
let totalIssues = 0;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
let warningCount = 0;
|
let warningCount = 0;
|
||||||
let infoCount = 0;
|
let infoCount = 0;
|
||||||
|
|
||||||
for (const file of filesToCheck) {
|
for (const file of filesToCheck) {
|
||||||
const fileIssues = findFileIssues(file);
|
const fileIssues = findFileIssues(file);
|
||||||
if (fileIssues.length > 0) {
|
if (fileIssues.length > 0) {
|
||||||
console.error(`\n ${file}`);
|
console.error(`\n[FILE] ${file}`);
|
||||||
for (const issue of fileIssues) {
|
for (const issue of fileIssues) {
|
||||||
const icon = issue.severity === 'error' ? 'FAIL:' : issue.severity === 'warning' ? 'WARNING:' : '';
|
const label = issue.severity === 'error' ? 'ERROR' : issue.severity === 'warning' ? 'WARNING' : 'INFO';
|
||||||
console.error(` ${icon} Line ${issue.line}: ${issue.message}`);
|
console.error(` ${label} Line ${issue.line}: ${issue.message}`);
|
||||||
totalIssues++;
|
totalIssues++;
|
||||||
if (issue.severity === 'error') errorCount++;
|
if (issue.severity === 'error') errorCount++;
|
||||||
if (issue.severity === 'warning') warningCount++;
|
if (issue.severity === 'warning') warningCount++;
|
||||||
@@ -317,51 +317,51 @@ function evaluate(rawInput) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate commit message if provided
|
// Validate commit message if provided
|
||||||
const messageValidation = validateCommitMessage(command);
|
const messageValidation = validateCommitMessage(command);
|
||||||
if (messageValidation && messageValidation.issues.length > 0) {
|
if (messageValidation && messageValidation.issues.length > 0) {
|
||||||
console.error('\n Commit Message Issues:');
|
console.error('\nCommit Message Issues:');
|
||||||
for (const issue of messageValidation.issues) {
|
for (const issue of messageValidation.issues) {
|
||||||
console.error(` WARNING: ${issue.message}`);
|
console.error(` WARNING ${issue.message}`);
|
||||||
if (issue.suggestion) {
|
if (issue.suggestion) {
|
||||||
console.error(` ${issue.suggestion}`);
|
console.error(` TIP ${issue.suggestion}`);
|
||||||
}
|
}
|
||||||
totalIssues++;
|
totalIssues++;
|
||||||
warningCount++;
|
warningCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run linter
|
// Run linter
|
||||||
const lintResults = runLinter(filesToCheck);
|
const lintResults = runLinter(filesToCheck);
|
||||||
|
|
||||||
if (lintResults.eslint && !lintResults.eslint.success) {
|
if (lintResults.eslint && !lintResults.eslint.success) {
|
||||||
console.error('\n ESLint Issues:');
|
console.error('\nESLint Issues:');
|
||||||
console.error(lintResults.eslint.output);
|
console.error(lintResults.eslint.output);
|
||||||
totalIssues++;
|
totalIssues++;
|
||||||
errorCount++;
|
errorCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lintResults.pylint && !lintResults.pylint.success) {
|
if (lintResults.pylint && !lintResults.pylint.success) {
|
||||||
console.error('\n Pylint Issues:');
|
console.error('\nPylint Issues:');
|
||||||
console.error(lintResults.pylint.output);
|
console.error(lintResults.pylint.output);
|
||||||
totalIssues++;
|
totalIssues++;
|
||||||
errorCount++;
|
errorCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lintResults.golint && !lintResults.golint.success) {
|
if (lintResults.golint && !lintResults.golint.success) {
|
||||||
console.error('\n golint Issues:');
|
console.error('\ngolint Issues:');
|
||||||
console.error(lintResults.golint.output);
|
console.error(lintResults.golint.output);
|
||||||
totalIssues++;
|
totalIssues++;
|
||||||
errorCount++;
|
errorCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
if (totalIssues > 0) {
|
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) {
|
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 };
|
return { output: rawInput, exitCode: 2 };
|
||||||
} else {
|
} else {
|
||||||
console.error('\n[Hook] WARNING: Warnings found. Consider fixing them, but commit is allowed.');
|
console.error('\n[Hook] WARNING: Warnings found. Consider fixing them, but commit is allowed.');
|
||||||
@@ -370,12 +370,12 @@ function evaluate(rawInput) {
|
|||||||
} else {
|
} else {
|
||||||
console.error('\n[Hook] PASS: All checks passed!');
|
console.error('\n[Hook] PASS: All checks passed!');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Hook] Error: ${error.message}`);
|
console.error(`[Hook] Error: ${error.message}`);
|
||||||
// Non-blocking on error
|
// Non-blocking on error
|
||||||
}
|
}
|
||||||
|
|
||||||
return { output: rawInput, exitCode: 0 };
|
return { output: rawInput, exitCode: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,14 +387,14 @@ function run(rawInput) {
|
|||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
let data = '';
|
let data = '';
|
||||||
process.stdin.setEncoding('utf8');
|
process.stdin.setEncoding('utf8');
|
||||||
|
|
||||||
process.stdin.on('data', chunk => {
|
process.stdin.on('data', chunk => {
|
||||||
if (data.length < MAX_STDIN) {
|
if (data.length < MAX_STDIN) {
|
||||||
const remaining = MAX_STDIN - data.length;
|
const remaining = MAX_STDIN - data.length;
|
||||||
data += chunk.substring(0, remaining);
|
data += chunk.substring(0, remaining);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
process.stdin.on('end', () => {
|
process.stdin.on('end', () => {
|
||||||
const result = evaluate(data);
|
const result = evaluate(data);
|
||||||
process.stdout.write(result.output);
|
process.stdout.write(result.output);
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function sleep(ms) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function animateProgress(label, steps, callback) {
|
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++) {
|
for (let i = 0; i < steps.length; i++) {
|
||||||
const step = steps[i];
|
const step = steps[i];
|
||||||
@@ -72,7 +72,7 @@ async function animateProgress(label, steps, callback) {
|
|||||||
await sleep(step.duration || 500);
|
await sleep(step.duration || 500);
|
||||||
process.stdout.clearLine?.(0) || process.stdout.write('\r');
|
process.stdout.clearLine?.(0) || process.stdout.write('\r');
|
||||||
process.stdout.cursorTo?.(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);
|
if (callback) callback(step, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ class SkillCreateOutput {
|
|||||||
|
|
||||||
console.log('\n');
|
console.log('\n');
|
||||||
console.log(chalk.bold(chalk.magenta('╔════════════════════════════════════════════════════════════════╗')));
|
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('║')) + ` ${subtitle}${' '.repeat(Math.max(0, 59 - stripAnsi(subtitle).length))}` + chalk.bold(chalk.magenta('║')));
|
||||||
console.log(chalk.bold(chalk.magenta('╚════════════════════════════════════════════════════════════════╝')));
|
console.log(chalk.bold(chalk.magenta('╚════════════════════════════════════════════════════════════════╝')));
|
||||||
console.log('');
|
console.log('');
|
||||||
@@ -111,7 +111,7 @@ class SkillCreateOutput {
|
|||||||
|
|
||||||
analysisResults(data) {
|
analysisResults(data) {
|
||||||
console.log('\n');
|
console.log('\n');
|
||||||
console.log(box(' Analysis Results', `
|
console.log(box('Analysis Results', `
|
||||||
${chalk.bold('Commits Analyzed:')} ${chalk.yellow(data.commits)}
|
${chalk.bold('Commits Analyzed:')} ${chalk.yellow(data.commits)}
|
||||||
${chalk.bold('Time Range:')} ${chalk.gray(data.timeRange)}
|
${chalk.bold('Time Range:')} ${chalk.gray(data.timeRange)}
|
||||||
${chalk.bold('Contributors:')} ${chalk.cyan(data.contributors)}
|
${chalk.bold('Contributors:')} ${chalk.cyan(data.contributors)}
|
||||||
@@ -121,7 +121,7 @@ ${chalk.bold('Files Tracked:')} ${chalk.green(data.files)}
|
|||||||
|
|
||||||
patterns(patterns) {
|
patterns(patterns) {
|
||||||
console.log('\n');
|
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)));
|
console.log(chalk.gray('─'.repeat(50)));
|
||||||
|
|
||||||
patterns.forEach((pattern, i) => {
|
patterns.forEach((pattern, i) => {
|
||||||
@@ -137,26 +137,26 @@ ${chalk.bold('Files Tracked:')} ${chalk.green(data.files)}
|
|||||||
|
|
||||||
instincts(instincts) {
|
instincts(instincts) {
|
||||||
console.log('\n');
|
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)}%)`)}`
|
`${chalk.yellow(`${i + 1}.`)} ${chalk.bold(inst.name)} ${chalk.gray(`(${Math.round(inst.confidence * 100)}%)`)}`
|
||||||
).join('\n')));
|
).join('\n')));
|
||||||
}
|
}
|
||||||
|
|
||||||
output(skillPath, instinctsPath) {
|
output(skillPath, instinctsPath) {
|
||||||
console.log('\n');
|
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.gray('─'.repeat(50)));
|
||||||
console.log(`
|
console.log(`
|
||||||
${chalk.green('')} ${chalk.bold('Skill File:')}
|
${chalk.green('-')} ${chalk.bold('Skill File:')}
|
||||||
${chalk.cyan(skillPath)}
|
${chalk.cyan(skillPath)}
|
||||||
|
|
||||||
${chalk.green('')} ${chalk.bold('Instincts File:')}
|
${chalk.green('-')} ${chalk.bold('Instincts File:')}
|
||||||
${chalk.cyan(instinctsPath)}
|
${chalk.cyan(instinctsPath)}
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
nextSteps() {
|
nextSteps() {
|
||||||
console.log(box(' Next Steps', `
|
console.log(box('Next Steps', `
|
||||||
${chalk.yellow('1.')} Review the generated SKILL.md
|
${chalk.yellow('1.')} Review the generated SKILL.md
|
||||||
${chalk.yellow('2.')} Import instincts: ${chalk.cyan('/instinct-import <path>')}
|
${chalk.yellow('2.')} Import instincts: ${chalk.cyan('/instinct-import <path>')}
|
||||||
${chalk.yellow('3.')} View learned patterns: ${chalk.cyan('/instinct-status')}
|
${chalk.yellow('3.')} View learned patterns: ${chalk.cyan('/instinct-status')}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ if (projectPath && projectPath !== cwd) {
|
|||||||
if (existsSync(projectPath)) {
|
if (existsSync(projectPath)) {
|
||||||
console.log(`→ cd ${projectPath}`);
|
console.log(`→ cd ${projectPath}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`WARNING: Path not found: ${projectPath}`);
|
console.log(`WARNING Path not found: ${projectPath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ function main() {
|
|||||||
// Check if previous session ID exists in sessions array
|
// Check if previous session ID exists in sessions array
|
||||||
const alreadySaved = context.sessions?.some(s => s.id === prevSession.sessionId);
|
const alreadySaved = context.sessions?.some(s => s.id === prevSession.sessionId);
|
||||||
if (!alreadySaved) {
|
if (!alreadySaved) {
|
||||||
summaryLines.push(`WARNING: Last session wasn't saved — run /ck:save to capture it`);
|
summaryLines.push(`WARNING Last session wasn't saved — run /ck:save to capture it`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ function main() {
|
|||||||
const claudeMdGoal = extractClaudeMdGoal(cwd);
|
const claudeMdGoal = extractClaudeMdGoal(cwd);
|
||||||
if (claudeMdGoal && context.goal &&
|
if (claudeMdGoal && context.goal &&
|
||||||
claudeMdGoal.toLowerCase().trim() !== context.goal.toLowerCase().trim()) {
|
claudeMdGoal.toLowerCase().trim() !== context.goal.toLowerCase().trim()) {
|
||||||
summaryLines.push(`WARNING: Goal mismatch — ck: "${context.goal.slice(0, 40)}" · CLAUDE.md: "${claudeMdGoal.slice(0, 40)}"`);
|
summaryLines.push(`WARNING Goal mismatch — ck: "${context.goal.slice(0, 40)}" · CLAUDE.md: "${claudeMdGoal.slice(0, 40)}"`);
|
||||||
summaryLines.push(` Run /ck:save with updated goal to sync`);
|
summaryLines.push(` Run /ck:save with updated goal to sync`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ function main() {
|
|||||||
'```',
|
'```',
|
||||||
``,
|
``,
|
||||||
`After the block, add one line: "Ready — what are we working on?"`,
|
`After the block, add one line: "Ready — what are we working on?"`,
|
||||||
`If you see WARNING: warnings above, mention them briefly after the block.`,
|
`If you see WARNING lines above, mention them briefly after the block.`,
|
||||||
].join('\n'));
|
].join('\n'));
|
||||||
|
|
||||||
return parts;
|
return parts;
|
||||||
|
|||||||
@@ -4,16 +4,16 @@
|
|||||||
# Called by observer-loop.sh before spawning any Claude session.
|
# Called by observer-loop.sh before spawning any Claude session.
|
||||||
#
|
#
|
||||||
# Config (env vars, all optional):
|
# Config (env vars, all optional):
|
||||||
# OBSERVER_INTERVAL_SECONDS default: 300 (per-project cooldown)
|
# OBSERVER_INTERVAL_SECONDS default: 300 (per-project cooldown)
|
||||||
# OBSERVER_LAST_RUN_LOG default: ~/.claude/observer-last-run.log
|
# OBSERVER_LAST_RUN_LOG default: ~/.claude/observer-last-run.log
|
||||||
# OBSERVER_ACTIVE_HOURS_START default: 800 (8:00 AM local, set to 0 to disable)
|
# OBSERVER_ACTIVE_HOURS_START default: 800 (8:00 AM local, set to 0 to disable)
|
||||||
# OBSERVER_ACTIVE_HOURS_END default: 2300 (11:00 PM local, set to 0 to disable)
|
# OBSERVER_ACTIVE_HOURS_END default: 2300 (11:00 PM local, set to 0 to disable)
|
||||||
# OBSERVER_MAX_IDLE_SECONDS default: 1800 (30 min; set to 0 to disable)
|
# OBSERVER_MAX_IDLE_SECONDS default: 1800 (30 min; set to 0 to disable)
|
||||||
#
|
#
|
||||||
# Gate execution order (cheapest first):
|
# Gate execution order (cheapest first):
|
||||||
# Gate 1: Time window check (~0ms, string comparison)
|
# Gate 1: Time window check (~0ms, string comparison)
|
||||||
# Gate 2: Project cooldown log (~1ms, file read + mkdir lock)
|
# Gate 2: Project cooldown log (~1ms, file read + mkdir lock)
|
||||||
# Gate 3: Idle detection (~5-50ms, OS syscall; fail open)
|
# Gate 3: Idle detection (~5-50ms, OS syscall; fail open)
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
# and creates instincts. Uses Haiku model for cost efficiency.
|
# and creates instincts. Uses Haiku model for cost efficiency.
|
||||||
#
|
#
|
||||||
# v2.1: Project-scoped — detects current project and analyzes
|
# v2.1: Project-scoped — detects current project and analyzes
|
||||||
# project-specific observations into project-scoped instincts.
|
# project-specific observations into project-scoped instincts.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# start-observer.sh # Start observer for current project (or global)
|
# start-observer.sh # Start observer for current project (or global)
|
||||||
# start-observer.sh --reset # Clear lock and restart observer for current project
|
# start-observer.sh --reset # Clear lock and restart observer for current project
|
||||||
# start-observer.sh stop # Stop running observer
|
# start-observer.sh stop # Stop running observer
|
||||||
# start-observer.sh status # Check if observer is running
|
# start-observer.sh status # Check if observer is running
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# Claude Code passes hook data via stdin as JSON.
|
# Claude Code passes hook data via stdin as JSON.
|
||||||
#
|
#
|
||||||
# v2.1: Project-scoped observations — detects current project context
|
# v2.1: Project-scoped observations — detects current project context
|
||||||
# and writes observations to project-specific directory.
|
# and writes observations to project-specific directory.
|
||||||
#
|
#
|
||||||
# Registered via plugin hooks/hooks.json (auto-loaded when plugin is enabled).
|
# Registered via plugin hooks/hooks.json (auto-loaded when plugin is enabled).
|
||||||
# Can also be registered manually in ~/.claude/settings.json.
|
# Can also be registered manually in ~/.claude/settings.json.
|
||||||
@@ -92,9 +92,9 @@ if [ -n "${CLV2_CONFIG:-}" ] && [ -f "$(dirname "$CLV2_CONFIG")/disabled" ]; the
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Prevent observe.sh from firing on non-human sessions to avoid:
|
# Prevent observe.sh from firing on non-human sessions to avoid:
|
||||||
# - ECC observing its own Haiku observer sessions (self-loop)
|
# - ECC observing its own Haiku observer sessions (self-loop)
|
||||||
# - ECC observing other tools' automated sessions
|
# - ECC observing other tools' automated sessions
|
||||||
# - automated sessions creating project-scoped homunculus metadata
|
# - automated sessions creating project-scoped homunculus metadata
|
||||||
|
|
||||||
# Layer 1: entrypoint. Only interactive terminal sessions should continue.
|
# Layer 1: entrypoint. Only interactive terminal sessions should continue.
|
||||||
# sdk-ts: Agent SDK sessions can be human-interactive (e.g. via Happy).
|
# sdk-ts: Agent SDK sessions can be human-interactive (e.g. via Happy).
|
||||||
|
|||||||
@@ -5,19 +5,19 @@
|
|||||||
# Sourced by observe.sh and start-observer.sh.
|
# Sourced by observe.sh and start-observer.sh.
|
||||||
#
|
#
|
||||||
# Exports:
|
# Exports:
|
||||||
# _CLV2_PROJECT_ID - Short hash identifying the project (or "global")
|
# _CLV2_PROJECT_ID - Short hash identifying the project (or "global")
|
||||||
# _CLV2_PROJECT_NAME - Human-readable project name
|
# _CLV2_PROJECT_NAME - Human-readable project name
|
||||||
# _CLV2_PROJECT_ROOT - Absolute path to project root
|
# _CLV2_PROJECT_ROOT - Absolute path to project root
|
||||||
# _CLV2_PROJECT_DIR - Project-scoped storage directory under homunculus
|
# _CLV2_PROJECT_DIR - Project-scoped storage directory under homunculus
|
||||||
#
|
#
|
||||||
# Also sets unprefixed convenience aliases:
|
# Also sets unprefixed convenience aliases:
|
||||||
# PROJECT_ID, PROJECT_NAME, PROJECT_ROOT, PROJECT_DIR
|
# PROJECT_ID, PROJECT_NAME, PROJECT_ROOT, PROJECT_DIR
|
||||||
#
|
#
|
||||||
# Detection priority:
|
# Detection priority:
|
||||||
# 1. CLAUDE_PROJECT_DIR env var (if set)
|
# 1. CLAUDE_PROJECT_DIR env var (if set)
|
||||||
# 2. git remote URL (hashed for uniqueness across machines)
|
# 2. git remote URL (hashed for uniqueness across machines)
|
||||||
# 3. git repo root path (fallback, machine-specific)
|
# 3. git repo root path (fallback, machine-specific)
|
||||||
# 4. "global" (no project context detected)
|
# 4. "global" (no project context detected)
|
||||||
|
|
||||||
_CLV2_HOMUNCULUS_DIR="${HOME}/.claude/homunculus"
|
_CLV2_HOMUNCULUS_DIR="${HOME}/.claude/homunculus"
|
||||||
_CLV2_PROJECTS_DIR="${_CLV2_HOMUNCULUS_DIR}/projects"
|
_CLV2_PROJECTS_DIR="${_CLV2_HOMUNCULUS_DIR}/projects"
|
||||||
|
|||||||
@@ -8,15 +8,15 @@
|
|||||||
#
|
#
|
||||||
# Hook config (in ~/.claude/settings.json):
|
# Hook config (in ~/.claude/settings.json):
|
||||||
# {
|
# {
|
||||||
# "hooks": {
|
# "hooks": {
|
||||||
# "Stop": [{
|
# "Stop": [{
|
||||||
# "matcher": "*",
|
# "matcher": "*",
|
||||||
# "hooks": [{
|
# "hooks": [{
|
||||||
# "type": "command",
|
# "type": "command",
|
||||||
# "command": "~/.claude/skills/continuous-learning/evaluate-session.sh"
|
# "command": "~/.claude/skills/continuous-learning/evaluate-session.sh"
|
||||||
# }]
|
# }]
|
||||||
# }]
|
# }]
|
||||||
# }
|
# }
|
||||||
# }
|
# }
|
||||||
#
|
#
|
||||||
# Patterns to detect: error_resolution, debugging_techniques, workarounds, project_specific
|
# Patterns to detect: error_resolution, debugging_techniques, workarounds, project_specific
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# Output: JSON to stdout
|
# Output: JSON to stdout
|
||||||
#
|
#
|
||||||
# Environment:
|
# Environment:
|
||||||
# RULES_DISTILL_DIR Override ~/.claude/rules (for testing only)
|
# RULES_DISTILL_DIR Override ~/.claude/rules (for testing only)
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
# script always picks up project-level skills without relying on the caller.
|
# script always picks up project-level skills without relying on the caller.
|
||||||
#
|
#
|
||||||
# Environment:
|
# Environment:
|
||||||
# RULES_DISTILL_GLOBAL_DIR Override ~/.claude/skills (for testing only;
|
# RULES_DISTILL_GLOBAL_DIR Override ~/.claude/skills (for testing only;
|
||||||
# do not set in production — intended for bats tests)
|
# do not set in production — intended for bats tests)
|
||||||
# RULES_DISTILL_PROJECT_DIR Override project dir detection (for testing only)
|
# RULES_DISTILL_PROJECT_DIR Override project dir detection (for testing only)
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
# script always picks up project-level skills without relying on the caller.
|
# script always picks up project-level skills without relying on the caller.
|
||||||
#
|
#
|
||||||
# Environment:
|
# Environment:
|
||||||
# SKILL_STOCKTAKE_GLOBAL_DIR Override ~/.claude/skills (for testing only;
|
# SKILL_STOCKTAKE_GLOBAL_DIR Override ~/.claude/skills (for testing only;
|
||||||
# do not set in production — intended for bats tests)
|
# do not set in production — intended for bats tests)
|
||||||
# SKILL_STOCKTAKE_PROJECT_DIR Override project dir detection (for testing only)
|
# SKILL_STOCKTAKE_PROJECT_DIR Override project dir detection (for testing only)
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# Usage: save-results.sh RESULTS_JSON <<< "$EVAL_JSON"
|
# Usage: save-results.sh RESULTS_JSON <<< "$EVAL_JSON"
|
||||||
#
|
#
|
||||||
# stdin format:
|
# stdin format:
|
||||||
# { "skills": {...}, "mode"?: "full"|"quick", "batch_progress"?: {...} }
|
# { "skills": {...}, "mode"?: "full"|"quick", "batch_progress"?: {...} }
|
||||||
#
|
#
|
||||||
# Always sets evaluated_at to current UTC time via `date -u`.
|
# Always sets evaluated_at to current UTC time via `date -u`.
|
||||||
# Merges stdin .skills into existing results.json (new entries override old).
|
# Merges stdin .skills into existing results.json (new entries override old).
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
# script always picks up project-level skills without relying on the caller.
|
# script always picks up project-level skills without relying on the caller.
|
||||||
#
|
#
|
||||||
# Environment:
|
# Environment:
|
||||||
# SKILL_STOCKTAKE_GLOBAL_DIR Override ~/.claude/skills (for testing only;
|
# SKILL_STOCKTAKE_GLOBAL_DIR Override ~/.claude/skills (for testing only;
|
||||||
# do not set in production — intended for bats tests)
|
# do not set in production — intended for bats tests)
|
||||||
# SKILL_STOCKTAKE_PROJECT_DIR Override project dir detection (for testing only)
|
# SKILL_STOCKTAKE_PROJECT_DIR Override project dir detection (for testing only)
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,15 @@
|
|||||||
#
|
#
|
||||||
# Hook config (in ~/.claude/settings.json):
|
# Hook config (in ~/.claude/settings.json):
|
||||||
# {
|
# {
|
||||||
# "hooks": {
|
# "hooks": {
|
||||||
# "PreToolUse": [{
|
# "PreToolUse": [{
|
||||||
# "matcher": "Edit|Write",
|
# "matcher": "Edit|Write",
|
||||||
# "hooks": [{
|
# "hooks": [{
|
||||||
# "type": "command",
|
# "type": "command",
|
||||||
# "command": "~/.claude/skills/strategic-compact/suggest-compact.sh"
|
# "command": "~/.claude/skills/strategic-compact/suggest-compact.sh"
|
||||||
# }]
|
# }]
|
||||||
# }]
|
# }]
|
||||||
# }
|
# }
|
||||||
# }
|
# }
|
||||||
#
|
#
|
||||||
# Criteria for suggesting compact:
|
# Criteria for suggesting compact:
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ def ensure_private_dir(path: Path) -> Path:
|
|||||||
def parse_args() -> tuple[bool, Path]:
|
def parse_args() -> tuple[bool, Path]:
|
||||||
clear = False
|
clear = False
|
||||||
output_dir: str | None = None
|
output_dir: str | None = None
|
||||||
|
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if arg == "--clear":
|
if arg == "--clear":
|
||||||
@@ -87,7 +87,7 @@ def parse_args() -> tuple[bool, Path]:
|
|||||||
raise SystemExit(f"Unknown flag: {arg}")
|
raise SystemExit(f"Unknown flag: {arg}")
|
||||||
elif not arg.startswith("-"):
|
elif not arg.startswith("-"):
|
||||||
output_dir = arg
|
output_dir = arg
|
||||||
|
|
||||||
if output_dir is None:
|
if output_dir is None:
|
||||||
events_dir = os.environ.get("VIDEODB_EVENTS_DIR")
|
events_dir = os.environ.get("VIDEODB_EVENTS_DIR")
|
||||||
if events_dir:
|
if events_dir:
|
||||||
@@ -147,10 +147,10 @@ def is_fatal_error(exc: Exception) -> bool:
|
|||||||
async def listen_with_retry():
|
async def listen_with_retry():
|
||||||
"""Main listen loop with auto-reconnect and exponential backoff."""
|
"""Main listen loop with auto-reconnect and exponential backoff."""
|
||||||
global _first_connection
|
global _first_connection
|
||||||
|
|
||||||
retry_count = 0
|
retry_count = 0
|
||||||
backoff = INITIAL_BACKOFF
|
backoff = INITIAL_BACKOFF
|
||||||
|
|
||||||
while retry_count < MAX_RETRIES:
|
while retry_count < MAX_RETRIES:
|
||||||
try:
|
try:
|
||||||
conn = videodb.connect()
|
conn = videodb.connect()
|
||||||
@@ -168,11 +168,11 @@ async def listen_with_retry():
|
|||||||
raise
|
raise
|
||||||
retry_count += 1
|
retry_count += 1
|
||||||
log(f"Connection error: {e}")
|
log(f"Connection error: {e}")
|
||||||
|
|
||||||
if retry_count >= MAX_RETRIES:
|
if retry_count >= MAX_RETRIES:
|
||||||
log(f"Max retries ({MAX_RETRIES}) exceeded, exiting")
|
log(f"Max retries ({MAX_RETRIES}) exceeded, exiting")
|
||||||
break
|
break
|
||||||
|
|
||||||
log(f"Reconnecting in {backoff}s (attempt {retry_count}/{MAX_RETRIES})...")
|
log(f"Reconnecting in {backoff}s (attempt {retry_count}/{MAX_RETRIES})...")
|
||||||
await asyncio.sleep(backoff)
|
await asyncio.sleep(backoff)
|
||||||
backoff = min(backoff * 2, MAX_BACKOFF)
|
backoff = min(backoff * 2, MAX_BACKOFF)
|
||||||
@@ -233,20 +233,20 @@ async def main_async():
|
|||||||
"""Async main with signal handling."""
|
"""Async main with signal handling."""
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
shutdown_event = asyncio.Event()
|
shutdown_event = asyncio.Event()
|
||||||
|
|
||||||
def handle_signal():
|
def handle_signal():
|
||||||
log("Received shutdown signal")
|
log("Received shutdown signal")
|
||||||
shutdown_event.set()
|
shutdown_event.set()
|
||||||
|
|
||||||
# Register signal handlers
|
# Register signal handlers
|
||||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||||
with contextlib.suppress(NotImplementedError):
|
with contextlib.suppress(NotImplementedError):
|
||||||
loop.add_signal_handler(sig, handle_signal)
|
loop.add_signal_handler(sig, handle_signal)
|
||||||
|
|
||||||
# Run listener with cancellation support
|
# Run listener with cancellation support
|
||||||
listen_task = asyncio.create_task(listen_with_retry())
|
listen_task = asyncio.create_task(listen_with_retry())
|
||||||
shutdown_task = asyncio.create_task(shutdown_event.wait())
|
shutdown_task = asyncio.create_task(shutdown_event.wait())
|
||||||
|
|
||||||
_done, pending = await asyncio.wait(
|
_done, pending = await asyncio.wait(
|
||||||
[listen_task, shutdown_task],
|
[listen_task, shutdown_task],
|
||||||
return_when=asyncio.FIRST_COMPLETED,
|
return_when=asyncio.FIRST_COMPLETED,
|
||||||
@@ -254,7 +254,7 @@ async def main_async():
|
|||||||
|
|
||||||
if listen_task.done():
|
if listen_task.done():
|
||||||
await listen_task
|
await listen_task
|
||||||
|
|
||||||
# Cancel remaining tasks
|
# Cancel remaining tasks
|
||||||
for task in pending:
|
for task in pending:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
@@ -266,7 +266,7 @@ async def main_async():
|
|||||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||||
with contextlib.suppress(NotImplementedError):
|
with contextlib.suppress(NotImplementedError):
|
||||||
loop.remove_signal_handler(sig)
|
loop.remove_signal_handler(sig)
|
||||||
|
|
||||||
log("Shutdown complete")
|
log("Shutdown complete")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ function resetAliases() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runTests() {
|
function runTests() {
|
||||||
|
const rocketEmoji = String.fromCodePoint(0x1F680);
|
||||||
console.log('\n=== Testing session-aliases.js ===\n');
|
console.log('\n=== Testing session-aliases.js ===\n');
|
||||||
|
|
||||||
let passed = 0;
|
let passed = 0;
|
||||||
@@ -1441,7 +1442,7 @@ function runTests() {
|
|||||||
'CJK characters should be rejected');
|
'CJK characters should be rejected');
|
||||||
|
|
||||||
// Emoji
|
// Emoji
|
||||||
const emojiResult = aliases.resolveAlias('rocket-');
|
const emojiResult = aliases.resolveAlias(`rocket-${rocketEmoji}`);
|
||||||
assert.strictEqual(emojiResult, null,
|
assert.strictEqual(emojiResult, null,
|
||||||
'Emoji should be rejected by the ASCII-only regex');
|
'Emoji should be rejected by the ASCII-only regex');
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ function test(name, fn) {
|
|||||||
|
|
||||||
// Test suite
|
// Test suite
|
||||||
function runTests() {
|
function runTests() {
|
||||||
|
const rocketParty = String.fromCodePoint(0x1F680, 0x1F389);
|
||||||
|
const partyEmoji = String.fromCodePoint(0x1F389);
|
||||||
console.log('\n=== Testing utils.js ===\n');
|
console.log('\n=== Testing utils.js ===\n');
|
||||||
|
|
||||||
let passed = 0;
|
let passed = 0;
|
||||||
@@ -166,9 +168,12 @@ function runTests() {
|
|||||||
if (test('sanitizeSessionId returns stable hashes for non-ASCII values', () => {
|
if (test('sanitizeSessionId returns stable hashes for non-ASCII values', () => {
|
||||||
const chinese = utils.sanitizeSessionId('我的项目');
|
const chinese = utils.sanitizeSessionId('我的项目');
|
||||||
const cyrillic = utils.sanitizeSessionId('проект');
|
const cyrillic = utils.sanitizeSessionId('проект');
|
||||||
|
const emoji = utils.sanitizeSessionId(rocketParty);
|
||||||
assert.ok(/^[a-f0-9]{8}$/.test(chinese), `Expected 8-char hash, got: ${chinese}`);
|
assert.ok(/^[a-f0-9]{8}$/.test(chinese), `Expected 8-char hash, got: ${chinese}`);
|
||||||
assert.ok(/^[a-f0-9]{8}$/.test(cyrillic), `Expected 8-char hash, got: ${cyrillic}`);
|
assert.ok(/^[a-f0-9]{8}$/.test(cyrillic), `Expected 8-char hash, got: ${cyrillic}`);
|
||||||
|
assert.ok(/^[a-f0-9]{8}$/.test(emoji), `Expected 8-char hash, got: ${emoji}`);
|
||||||
assert.notStrictEqual(chinese, cyrillic);
|
assert.notStrictEqual(chinese, cyrillic);
|
||||||
|
assert.notStrictEqual(chinese, emoji);
|
||||||
assert.strictEqual(utils.sanitizeSessionId('日本語プロジェクト'), utils.sanitizeSessionId('日本語プロジェクト'));
|
assert.strictEqual(utils.sanitizeSessionId('日本語プロジェクト'), utils.sanitizeSessionId('日本語プロジェクト'));
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
@@ -704,7 +709,7 @@ function runTests() {
|
|||||||
if (test('writeFile handles unicode content', () => {
|
if (test('writeFile handles unicode content', () => {
|
||||||
const testFile = path.join(utils.getTempDir(), `utils-test-${Date.now()}.txt`);
|
const testFile = path.join(utils.getTempDir(), `utils-test-${Date.now()}.txt`);
|
||||||
try {
|
try {
|
||||||
const unicode = '日本語テスト 中文 émojis';
|
const unicode = `日本語テスト ${String.fromCodePoint(0x1F680)} émojis`;
|
||||||
utils.writeFile(testFile, unicode);
|
utils.writeFile(testFile, unicode);
|
||||||
const content = utils.readFile(testFile);
|
const content = utils.readFile(testFile);
|
||||||
assert.strictEqual(content, unicode);
|
assert.strictEqual(content, unicode);
|
||||||
@@ -1868,18 +1873,18 @@ function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// ── Round 108: grepFile with Unicode content — UTF-16 string matching on split lines ──
|
// ── Round 108: grepFile with Unicode/emoji content — UTF-16 string matching on split lines ──
|
||||||
console.log('\nRound 108: grepFile (Unicode — regex matching on UTF-16 split lines):');
|
console.log('\nRound 108: grepFile (Unicode/emoji — regex matching on UTF-16 split lines):');
|
||||||
if (test('grepFile finds Unicode patterns across lines', () => {
|
if (test('grepFile finds Unicode emoji patterns across lines', () => {
|
||||||
const tmpDir = fs.mkdtempSync(path.join(utils.getTempDir(), 'r108-grep-unicode-'));
|
const tmpDir = fs.mkdtempSync(path.join(utils.getTempDir(), 'r108-grep-unicode-'));
|
||||||
const testFile = path.join(tmpDir, 'test.txt');
|
const testFile = path.join(tmpDir, 'test.txt');
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(testFile, '猫 celebration\nnormal line\n猫 party\n日本語テスト');
|
fs.writeFileSync(testFile, `${partyEmoji} celebration\nnormal line\n${partyEmoji} party\n日本語テスト`);
|
||||||
const unicodeResults = utils.grepFile(testFile, /猫/);
|
const emojiResults = utils.grepFile(testFile, new RegExp(partyEmoji, 'u'));
|
||||||
assert.strictEqual(unicodeResults.length, 2,
|
assert.strictEqual(emojiResults.length, 2,
|
||||||
'Should find Unicode matches on 2 lines (lines 1 and 3)');
|
'Should find emoji on 2 lines (lines 1 and 3)');
|
||||||
assert.strictEqual(unicodeResults[0].lineNumber, 1);
|
assert.strictEqual(emojiResults[0].lineNumber, 1);
|
||||||
assert.strictEqual(unicodeResults[1].lineNumber, 3);
|
assert.strictEqual(emojiResults[1].lineNumber, 3);
|
||||||
const cjkResults = utils.grepFile(testFile, /日本語/);
|
const cjkResults = utils.grepFile(testFile, /日本語/);
|
||||||
assert.strictEqual(cjkResults.length, 1,
|
assert.strictEqual(cjkResults.length, 1,
|
||||||
'Should find CJK characters on line 4');
|
'Should find CJK characters on line 4');
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ for (const testFile of testFiles) {
|
|||||||
const displayPath = testFile.split(path.sep).join('/');
|
const displayPath = testFile.split(path.sep).join('/');
|
||||||
|
|
||||||
if (!fs.existsSync(testPath)) {
|
if (!fs.existsSync(testPath)) {
|
||||||
console.log(`WARNING: Skipping ${displayPath} (file not found)`);
|
console.log(`WARNING Skipping ${displayPath} (file not found)`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ function makeTempRoot(prefix) {
|
|||||||
const warningEmoji = String.fromCodePoint(0x26A0, 0xFE0F);
|
const warningEmoji = String.fromCodePoint(0x26A0, 0xFE0F);
|
||||||
const toolsEmoji = String.fromCodePoint(0x1F6E0, 0xFE0F);
|
const toolsEmoji = String.fromCodePoint(0x1F6E0, 0xFE0F);
|
||||||
const zeroWidthSpace = String.fromCodePoint(0x200B);
|
const zeroWidthSpace = String.fromCodePoint(0x200B);
|
||||||
|
const rocketEmoji = String.fromCodePoint(0x1F680);
|
||||||
|
|
||||||
let passed = 0;
|
let passed = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
@@ -78,6 +79,36 @@ if (
|
|||||||
passed++;
|
passed++;
|
||||||
else failed++;
|
else failed++;
|
||||||
|
|
||||||
|
if (
|
||||||
|
test('write mode does not rewrite executable files', () => {
|
||||||
|
const root = makeTempRoot('ecc-unicode-code-');
|
||||||
|
fs.mkdirSync(path.join(root, 'scripts'), { recursive: true });
|
||||||
|
const scriptFile = path.join(root, 'scripts', 'sample.js');
|
||||||
|
const original = `const label = "Launch ${rocketEmoji}";\n`;
|
||||||
|
fs.writeFileSync(scriptFile, original);
|
||||||
|
|
||||||
|
const result = runCheck(root, ['--write']);
|
||||||
|
assert.notStrictEqual(result.status, 0, result.stdout + result.stderr);
|
||||||
|
assert.match(result.stderr, /scripts\/sample\.js:1:23 emoji U\+1F680/);
|
||||||
|
assert.strictEqual(fs.readFileSync(scriptFile, 'utf8'), original);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
passed++;
|
||||||
|
else failed++;
|
||||||
|
|
||||||
|
if (
|
||||||
|
test('plain symbols like copyright remain allowed', () => {
|
||||||
|
const root = makeTempRoot('ecc-unicode-symbols-');
|
||||||
|
fs.mkdirSync(path.join(root, 'docs'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(root, 'docs', 'legal.md'), 'Copyright © ECC\nTrademark ® ECC\n');
|
||||||
|
|
||||||
|
const result = runCheck(root);
|
||||||
|
assert.strictEqual(result.status, 0, result.stdout + result.stderr);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
passed++;
|
||||||
|
else failed++;
|
||||||
|
|
||||||
console.log(`\nPassed: ${passed}`);
|
console.log(`\nPassed: ${passed}`);
|
||||||
console.log(`Failed: ${failed}`);
|
console.log(`Failed: ${failed}`);
|
||||||
process.exit(failed > 0 ? 1 : 0);
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user