mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-19 16:43:29 +08:00
Compare commits
2 Commits
b2285e870a
...
7356fd996f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7356fd996f | ||
|
|
18c5a76a96 |
@@ -105,7 +105,7 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const{execFileSync}=require('child_process');const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path;if(p&&/\\.(ts|tsx|js|jsx)$/.test(p)&&fs.existsSync(p)){try{execFileSync('npx',['prettier','--write',p],{stdio:['pipe','pipe','pipe']})}catch(e){}}console.log(d)})\""
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/post-edit-format.js\""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Auto-format JS/TS files with Prettier after edits"
|
"description": "Auto-format JS/TS files with Prettier after edits"
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const{execFileSync}=require('child_process');const fs=require('fs');const path=require('path');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path;if(p&&/\\.(ts|tsx)$/.test(p)&&fs.existsSync(p)){let dir=path.dirname(p);while(dir!==path.dirname(dir)&&!fs.existsSync(path.join(dir,'tsconfig.json'))){dir=path.dirname(dir)}if(fs.existsSync(path.join(dir,'tsconfig.json'))){try{const r=execFileSync('npx',['tsc','--noEmit','--pretty','false'],{cwd:dir,encoding:'utf8',stdio:['pipe','pipe','pipe']});const lines=r.split('\\n').filter(l=>l.includes(p)).slice(0,10);if(lines.length)console.error(lines.join('\\n'))}catch(e){const lines=(e.stdout||'').split('\\n').filter(l=>l.includes(p)).slice(0,10);if(lines.length)console.error(lines.join('\\n'))}}}console.log(d)})\""
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/post-edit-typecheck.js\""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "TypeScript check after editing .ts/.tsx files"
|
"description": "TypeScript check after editing .ts/.tsx files"
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path;if(p&&/\\.(ts|tsx|js|jsx)$/.test(p)&&fs.existsSync(p)){const c=fs.readFileSync(p,'utf8');const lines=c.split('\\n');const matches=[];lines.forEach((l,idx)=>{if(/console\\.log/.test(l))matches.push((idx+1)+': '+l.trim())});if(matches.length){console.error('[Hook] WARNING: console.log found in '+p);matches.slice(0,5).forEach(m=>console.error(m));console.error('[Hook] Remove console.log before committing')}}console.log(d)})\""
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/post-edit-console-warn.js\""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Warn about console.log statements after edits"
|
"description": "Warn about console.log statements after edits"
|
||||||
|
|||||||
@@ -3,56 +3,60 @@
|
|||||||
/**
|
/**
|
||||||
* Stop Hook: Check for console.log statements in modified files
|
* Stop Hook: Check for console.log statements in modified files
|
||||||
*
|
*
|
||||||
* This hook runs after each response and checks if any modified
|
* Cross-platform (Windows, macOS, Linux)
|
||||||
* JavaScript/TypeScript files contain console.log statements.
|
*
|
||||||
* It provides warnings to help developers remember to remove
|
* Runs after each response and checks if any modified JavaScript/TypeScript
|
||||||
* debug statements before committing.
|
* files contain console.log statements. Provides warnings to help developers
|
||||||
|
* remember to remove debug statements before committing.
|
||||||
|
*
|
||||||
|
* Exclusions: test files, config files, and scripts/ directory (where
|
||||||
|
* console.log is often intentional).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { execSync } = require('child_process');
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const { isGitRepo, getGitModifiedFiles, log } = require('../lib/utils');
|
||||||
|
|
||||||
|
// Files where console.log is expected and should not trigger warnings
|
||||||
|
const EXCLUDED_PATTERNS = [
|
||||||
|
/\.test\.[jt]sx?$/,
|
||||||
|
/\.spec\.[jt]sx?$/,
|
||||||
|
/\.config\.[jt]s$/,
|
||||||
|
/scripts\//,
|
||||||
|
/__tests__\//,
|
||||||
|
/__mocks__\//,
|
||||||
|
];
|
||||||
|
|
||||||
let data = '';
|
let data = '';
|
||||||
|
|
||||||
// Read stdin
|
|
||||||
process.stdin.on('data', chunk => {
|
process.stdin.on('data', chunk => {
|
||||||
data += chunk;
|
data += chunk;
|
||||||
});
|
});
|
||||||
|
|
||||||
process.stdin.on('end', () => {
|
process.stdin.on('end', () => {
|
||||||
try {
|
try {
|
||||||
// Check if we're in a git repository
|
if (!isGitRepo()) {
|
||||||
try {
|
|
||||||
execSync('git rev-parse --git-dir', { stdio: 'pipe' });
|
|
||||||
} catch {
|
|
||||||
// Not in a git repo, just pass through the data
|
|
||||||
console.log(data);
|
console.log(data);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get list of modified files
|
const files = getGitModifiedFiles(['\\.tsx?$', '\\.jsx?$'])
|
||||||
const files = execSync('git diff --name-only HEAD', {
|
.filter(f => fs.existsSync(f))
|
||||||
encoding: 'utf8',
|
.filter(f => !EXCLUDED_PATTERNS.some(pattern => pattern.test(f)));
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
|
||||||
})
|
|
||||||
.split('\n')
|
|
||||||
.filter(f => /\.(ts|tsx|js|jsx)$/.test(f) && fs.existsSync(f));
|
|
||||||
|
|
||||||
let hasConsole = false;
|
let hasConsole = false;
|
||||||
|
|
||||||
// Check each file for console.log
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const content = fs.readFileSync(file, 'utf8');
|
const content = fs.readFileSync(file, 'utf8');
|
||||||
if (content.includes('console.log')) {
|
if (content.includes('console.log')) {
|
||||||
console.error(`[Hook] WARNING: console.log found in ${file}`);
|
log(`[Hook] WARNING: console.log found in ${file}`);
|
||||||
hasConsole = true;
|
hasConsole = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasConsole) {
|
if (hasConsole) {
|
||||||
console.error('[Hook] Remove console.log statements before committing');
|
log('[Hook] Remove console.log statements before committing');
|
||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch {
|
||||||
// Silently ignore errors (git might not be available, etc.)
|
// Silently ignore errors (git might not be available, etc.)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
47
scripts/hooks/post-edit-console-warn.js
Normal file
47
scripts/hooks/post-edit-console-warn.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* PostToolUse Hook: Warn about console.log statements after edits
|
||||||
|
*
|
||||||
|
* Cross-platform (Windows, macOS, Linux)
|
||||||
|
*
|
||||||
|
* Runs after Edit tool use. If the edited JS/TS file contains console.log
|
||||||
|
* statements, warns with line numbers to help remove debug statements
|
||||||
|
* before committing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
process.stdin.on('data', chunk => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdin.on('end', () => {
|
||||||
|
try {
|
||||||
|
const input = JSON.parse(data);
|
||||||
|
const filePath = input.tool_input?.file_path;
|
||||||
|
|
||||||
|
if (filePath && /\.(ts|tsx|js|jsx)$/.test(filePath) && fs.existsSync(filePath)) {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
const matches = [];
|
||||||
|
|
||||||
|
lines.forEach((line, idx) => {
|
||||||
|
if (/console\.log/.test(line)) {
|
||||||
|
matches.push((idx + 1) + ': ' + line.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matches.length > 0) {
|
||||||
|
console.error('[Hook] WARNING: console.log found in ' + filePath);
|
||||||
|
matches.slice(0, 5).forEach(m => console.error(m));
|
||||||
|
console.error('[Hook] Remove console.log before committing');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Invalid input — pass through
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
40
scripts/hooks/post-edit-format.js
Normal file
40
scripts/hooks/post-edit-format.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* PostToolUse Hook: Auto-format JS/TS files with Prettier after edits
|
||||||
|
*
|
||||||
|
* Cross-platform (Windows, macOS, Linux)
|
||||||
|
*
|
||||||
|
* Runs after Edit tool use. If the edited file is a JS/TS file,
|
||||||
|
* formats it with Prettier. Fails silently if Prettier isn't installed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { execFileSync } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
process.stdin.on('data', chunk => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdin.on('end', () => {
|
||||||
|
try {
|
||||||
|
const input = JSON.parse(data);
|
||||||
|
const filePath = input.tool_input?.file_path;
|
||||||
|
|
||||||
|
if (filePath && /\.(ts|tsx|js|jsx)$/.test(filePath) && fs.existsSync(filePath)) {
|
||||||
|
try {
|
||||||
|
execFileSync('npx', ['prettier', '--write', filePath], {
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
timeout: 15000
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Prettier not installed or failed — non-blocking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Invalid input — pass through
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
69
scripts/hooks/post-edit-typecheck.js
Normal file
69
scripts/hooks/post-edit-typecheck.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* PostToolUse Hook: TypeScript check after editing .ts/.tsx files
|
||||||
|
*
|
||||||
|
* Cross-platform (Windows, macOS, Linux)
|
||||||
|
*
|
||||||
|
* Runs after Edit tool use on TypeScript files. Walks up from the file's
|
||||||
|
* directory to find the nearest tsconfig.json, then runs tsc --noEmit
|
||||||
|
* and reports only errors related to the edited file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { execFileSync } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
process.stdin.on('data', chunk => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdin.on('end', () => {
|
||||||
|
try {
|
||||||
|
const input = JSON.parse(data);
|
||||||
|
const filePath = input.tool_input?.file_path;
|
||||||
|
|
||||||
|
if (filePath && /\.(ts|tsx)$/.test(filePath) && fs.existsSync(filePath)) {
|
||||||
|
// Find nearest tsconfig.json by walking up (max 20 levels to prevent infinite loop)
|
||||||
|
let dir = path.dirname(path.resolve(filePath));
|
||||||
|
const root = path.parse(dir).root;
|
||||||
|
let depth = 0;
|
||||||
|
|
||||||
|
while (dir !== root && depth < 20) {
|
||||||
|
if (fs.existsSync(path.join(dir, 'tsconfig.json'))) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dir = path.dirname(dir);
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(path.join(dir, 'tsconfig.json'))) {
|
||||||
|
try {
|
||||||
|
execFileSync('npx', ['tsc', '--noEmit', '--pretty', 'false'], {
|
||||||
|
cwd: dir,
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// tsc exits non-zero when there are errors — filter to edited file
|
||||||
|
const output = err.stdout || '';
|
||||||
|
const relevantLines = output
|
||||||
|
.split('\n')
|
||||||
|
.filter(line => line.includes(filePath) || line.includes(path.basename(filePath)))
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
|
if (relevantLines.length > 0) {
|
||||||
|
console.error('[Hook] TypeScript errors in ' + path.basename(filePath) + ':');
|
||||||
|
relevantLines.forEach(line => console.error(line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Invalid input — pass through
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
@@ -38,6 +38,7 @@ function extractSessionSummary(transcriptPath) {
|
|||||||
const userMessages = [];
|
const userMessages = [];
|
||||||
const toolsUsed = new Set();
|
const toolsUsed = new Set();
|
||||||
const filesModified = new Set();
|
const filesModified = new Set();
|
||||||
|
let parseErrors = 0;
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
try {
|
try {
|
||||||
@@ -66,10 +67,14 @@ function extractSessionSummary(transcriptPath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Skip unparseable lines
|
parseErrors++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parseErrors > 0) {
|
||||||
|
log(`[SessionEnd] Skipped ${parseErrors}/${lines.length} unparseable transcript lines`);
|
||||||
|
}
|
||||||
|
|
||||||
if (userMessages.length === 0) return null;
|
if (userMessages.length === 0) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ const {
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// Track tool call count (increment in a temp file)
|
// Track tool call count (increment in a temp file)
|
||||||
// Use a session-specific counter file based on PID from parent process
|
// Use a session-specific counter file based on session ID from environment
|
||||||
// or session ID from environment
|
// or parent PID as fallback
|
||||||
const sessionId = process.env.CLAUDE_SESSION_ID || process.ppid || 'default';
|
const sessionId = process.env.CLAUDE_SESSION_ID || String(process.ppid) || 'default';
|
||||||
const counterFile = path.join(getTempDir(), `claude-tool-count-${sessionId}`);
|
const counterFile = path.join(getTempDir(), `claude-tool-count-${sessionId}`);
|
||||||
const threshold = parseInt(process.env.COMPACT_THRESHOLD || '50', 10);
|
const threshold = parseInt(process.env.COMPACT_THRESHOLD || '50', 10);
|
||||||
|
|
||||||
@@ -34,7 +34,9 @@ async function main() {
|
|||||||
// Read existing count or start at 1
|
// Read existing count or start at 1
|
||||||
const existing = readFile(counterFile);
|
const existing = readFile(counterFile);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
count = parseInt(existing.trim(), 10) + 1;
|
const parsed = parseInt(existing.trim(), 10);
|
||||||
|
// Guard against NaN from corrupted counter file
|
||||||
|
count = Number.isFinite(parsed) ? parsed + 1 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updated count
|
// Save updated count
|
||||||
|
|||||||
@@ -156,11 +156,12 @@ function getAvailablePackageManagers() {
|
|||||||
* 5. Global user preference (in ~/.claude/package-manager.json)
|
* 5. Global user preference (in ~/.claude/package-manager.json)
|
||||||
* 6. Default to npm (no child processes spawned)
|
* 6. Default to npm (no child processes spawned)
|
||||||
*
|
*
|
||||||
* @param {object} options - { projectDir, fallbackOrder }
|
* @param {object} options - Options
|
||||||
|
* @param {string} options.projectDir - Project directory to detect from (default: cwd)
|
||||||
* @returns {object} - { name, config, source }
|
* @returns {object} - { name, config, source }
|
||||||
*/
|
*/
|
||||||
function getPackageManager(options = {}) {
|
function getPackageManager(options = {}) {
|
||||||
const { projectDir = process.cwd(), fallbackOrder = DETECTION_PRIORITY } = options;
|
const { projectDir = process.cwd() } = options;
|
||||||
|
|
||||||
// 1. Check environment variable
|
// 1. Check environment variable
|
||||||
const envPm = process.env.CLAUDE_PACKAGE_MANAGER;
|
const envPm = process.env.CLAUDE_PACKAGE_MANAGER;
|
||||||
|
|||||||
@@ -135,9 +135,13 @@ function saveAliases(aliases) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up temp file
|
// Clean up temp file (best-effort)
|
||||||
if (fs.existsSync(tempPath)) {
|
try {
|
||||||
fs.unlinkSync(tempPath);
|
if (fs.existsSync(tempPath)) {
|
||||||
|
fs.unlinkSync(tempPath);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Non-critical: temp file will be overwritten on next save
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -143,11 +143,21 @@ function parseSessionMetadata(content) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate statistics for a session
|
* Calculate statistics for a session
|
||||||
* @param {string} sessionPath - Full path to session file
|
* @param {string} sessionPathOrContent - Full path to session file, OR
|
||||||
|
* the pre-read content string (to avoid redundant disk reads when
|
||||||
|
* the caller already has the content loaded).
|
||||||
* @returns {object} Statistics object
|
* @returns {object} Statistics object
|
||||||
*/
|
*/
|
||||||
function getSessionStats(sessionPath) {
|
function getSessionStats(sessionPathOrContent) {
|
||||||
const content = getSessionContent(sessionPath);
|
// Accept pre-read content string to avoid redundant file reads.
|
||||||
|
// If the argument looks like a file path (no newlines, ends with .tmp),
|
||||||
|
// read from disk. Otherwise treat it as content.
|
||||||
|
const content = (typeof sessionPathOrContent === 'string' &&
|
||||||
|
!sessionPathOrContent.includes('\n') &&
|
||||||
|
sessionPathOrContent.endsWith('.tmp'))
|
||||||
|
? getSessionContent(sessionPathOrContent)
|
||||||
|
: sessionPathOrContent;
|
||||||
|
|
||||||
const metadata = parseSessionMetadata(content);
|
const metadata = parseSessionMetadata(content);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -281,7 +291,8 @@ function getSessionById(sessionId, includeContent = false) {
|
|||||||
if (includeContent) {
|
if (includeContent) {
|
||||||
session.content = getSessionContent(sessionPath);
|
session.content = getSessionContent(sessionPath);
|
||||||
session.metadata = parseSessionMetadata(session.content);
|
session.metadata = parseSessionMetadata(session.content);
|
||||||
session.stats = getSessionStats(sessionPath);
|
// Pass pre-read content to avoid a redundant disk read
|
||||||
|
session.stats = getSessionStats(session.content || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
|
|||||||
@@ -57,10 +57,20 @@ function getTempDir() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure a directory exists (create if not)
|
* Ensure a directory exists (create if not)
|
||||||
|
* @param {string} dirPath - Directory path to create
|
||||||
|
* @returns {string} The directory path
|
||||||
|
* @throws {Error} If directory cannot be created (e.g., permission denied)
|
||||||
*/
|
*/
|
||||||
function ensureDir(dirPath) {
|
function ensureDir(dirPath) {
|
||||||
if (!fs.existsSync(dirPath)) {
|
try {
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// EEXIST is fine (race condition with another process creating it)
|
||||||
|
if (err.code !== 'EEXIST') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return dirPath;
|
return dirPath;
|
||||||
}
|
}
|
||||||
@@ -187,10 +197,29 @@ function findFiles(dir, pattern, options = {}) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Read JSON from stdin (for hook input)
|
* Read JSON from stdin (for hook input)
|
||||||
|
* @param {object} options - Options
|
||||||
|
* @param {number} options.timeoutMs - Timeout in milliseconds (default: 5000).
|
||||||
|
* Prevents hooks from hanging indefinitely if stdin never closes.
|
||||||
|
* @returns {Promise<object>} Parsed JSON object, or empty object if stdin is empty
|
||||||
*/
|
*/
|
||||||
async function readStdinJson() {
|
async function readStdinJson(options = {}) {
|
||||||
|
const { timeoutMs = 5000 } = options;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let data = '';
|
let data = '';
|
||||||
|
let settled = false;
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (!settled) {
|
||||||
|
settled = true;
|
||||||
|
// Resolve with whatever we have so far rather than hanging
|
||||||
|
try {
|
||||||
|
resolve(data.trim() ? JSON.parse(data) : {});
|
||||||
|
} catch {
|
||||||
|
resolve({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
process.stdin.setEncoding('utf8');
|
process.stdin.setEncoding('utf8');
|
||||||
process.stdin.on('data', chunk => {
|
process.stdin.on('data', chunk => {
|
||||||
@@ -198,18 +227,22 @@ async function readStdinJson() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
process.stdin.on('end', () => {
|
process.stdin.on('end', () => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
try {
|
try {
|
||||||
if (data.trim()) {
|
resolve(data.trim() ? JSON.parse(data) : {});
|
||||||
resolve(JSON.parse(data));
|
|
||||||
} else {
|
|
||||||
resolve({});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
process.stdin.on('error', reject);
|
process.stdin.on('error', err => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +346,10 @@ function isGitRepo() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get git modified files
|
* Get git modified files, optionally filtered by regex patterns
|
||||||
|
* @param {string[]} patterns - Array of regex pattern strings to filter files.
|
||||||
|
* Invalid patterns are silently skipped.
|
||||||
|
* @returns {string[]} Array of modified file paths
|
||||||
*/
|
*/
|
||||||
function getGitModifiedFiles(patterns = []) {
|
function getGitModifiedFiles(patterns = []) {
|
||||||
if (!isGitRepo()) return [];
|
if (!isGitRepo()) return [];
|
||||||
@@ -324,12 +360,18 @@ function getGitModifiedFiles(patterns = []) {
|
|||||||
let files = result.output.split('\n').filter(Boolean);
|
let files = result.output.split('\n').filter(Boolean);
|
||||||
|
|
||||||
if (patterns.length > 0) {
|
if (patterns.length > 0) {
|
||||||
files = files.filter(file => {
|
// Pre-compile patterns, skipping invalid ones
|
||||||
return patterns.some(pattern => {
|
const compiled = [];
|
||||||
const regex = new RegExp(pattern);
|
for (const pattern of patterns) {
|
||||||
return regex.test(file);
|
try {
|
||||||
});
|
compiled.push(new RegExp(pattern));
|
||||||
});
|
} catch {
|
||||||
|
// Skip invalid regex patterns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (compiled.length > 0) {
|
||||||
|
files = files.filter(file => compiled.some(regex => regex.test(file)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
@@ -349,12 +391,23 @@ function replaceInFile(filePath, search, replace) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Count occurrences of a pattern in a file
|
* Count occurrences of a pattern in a file
|
||||||
|
* @param {string} filePath - Path to the file
|
||||||
|
* @param {string|RegExp} pattern - Pattern to count. Strings are treated as
|
||||||
|
* global regex patterns. RegExp instances are used as-is but the global
|
||||||
|
* flag is enforced to ensure correct counting.
|
||||||
|
* @returns {number} Number of matches found
|
||||||
*/
|
*/
|
||||||
function countInFile(filePath, pattern) {
|
function countInFile(filePath, pattern) {
|
||||||
const content = readFile(filePath);
|
const content = readFile(filePath);
|
||||||
if (content === null) return 0;
|
if (content === null) return 0;
|
||||||
|
|
||||||
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'g');
|
let regex;
|
||||||
|
if (pattern instanceof RegExp) {
|
||||||
|
// Ensure global flag is set for correct counting
|
||||||
|
regex = pattern.global ? pattern : new RegExp(pattern.source, pattern.flags + 'g');
|
||||||
|
} else {
|
||||||
|
regex = new RegExp(pattern, 'g');
|
||||||
|
}
|
||||||
const matches = content.match(regex);
|
const matches = content.match(regex);
|
||||||
return matches ? matches.length : 0;
|
return matches ? matches.length : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user