- Introduced coding standards for TypeScript and React in SKILL.md. - Added continuous learning skill with configuration and evaluation scripts. - Created detail layout guidelines for read-only pages. - Established form layout rules for data-entry forms. - Documented project guidelines for the frontend monorepo. - Implemented security review checklist for frontend/Electron applications. - Developed a verification loop skill for comprehensive session checks. This commit enhances the skill set available for developers, ensuring adherence to best practices and improving code quality.
395 lines
8.5 KiB
JavaScript
395 lines
8.5 KiB
JavaScript
/**
|
|
* Cross-platform utility functions for Cursor hooks and scripts
|
|
* Works on Windows, macOS, and Linux
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const { execSync, spawnSync } = require('child_process');
|
|
|
|
// Platform detection
|
|
const isWindows = process.platform === 'win32';
|
|
const isMacOS = process.platform === 'darwin';
|
|
const isLinux = process.platform === 'linux';
|
|
|
|
/**
|
|
* Get the user's home directory (cross-platform)
|
|
*/
|
|
function getHomeDir() {
|
|
return os.homedir();
|
|
}
|
|
|
|
/**
|
|
* Walk up from cwd to find the project root (directory with .cursor/)
|
|
*/
|
|
function getProjectRoot(startDir = process.cwd()) {
|
|
let dir = startDir;
|
|
while (true) {
|
|
if (fs.existsSync(path.join(dir, '.cursor')) && fs.existsSync(path.join(dir, '.agents'))) {
|
|
return dir;
|
|
}
|
|
const parent = path.dirname(dir);
|
|
if (parent === dir) {
|
|
return startDir;
|
|
}
|
|
dir = parent;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the user-level Cursor config directory
|
|
*/
|
|
function getCursorDir() {
|
|
return path.join(getHomeDir(), '.cursor');
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use getCursorDir() — kept so older scripts keep working
|
|
*/
|
|
function getClaudeDir() {
|
|
return getCursorDir();
|
|
}
|
|
|
|
/**
|
|
* Get the sessions directory (project-local)
|
|
*/
|
|
function getSessionsDir() {
|
|
return path.join(getProjectRoot(), '.cursor', 'sessions');
|
|
}
|
|
|
|
/**
|
|
* Get the learned skills directory (project-local)
|
|
*/
|
|
function getLearnedSkillsDir() {
|
|
return path.join(getProjectRoot(), '.agents', 'skills', 'learned');
|
|
}
|
|
|
|
/**
|
|
* Get the temp directory (cross-platform)
|
|
*/
|
|
function getTempDir() {
|
|
return os.tmpdir();
|
|
}
|
|
|
|
/**
|
|
* Ensure a directory exists (create if not)
|
|
*/
|
|
function ensureDir(dirPath) {
|
|
if (!fs.existsSync(dirPath)) {
|
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
}
|
|
return dirPath;
|
|
}
|
|
|
|
/**
|
|
* Get current date in YYYY-MM-DD format
|
|
*/
|
|
function getDateString() {
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
const day = String(now.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
/**
|
|
* Get current time in HH:MM format
|
|
*/
|
|
function getTimeString() {
|
|
const now = new Date();
|
|
const hours = String(now.getHours()).padStart(2, '0');
|
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
return `${hours}:${minutes}`;
|
|
}
|
|
|
|
/**
|
|
* Get current datetime in YYYY-MM-DD HH:MM:SS format
|
|
*/
|
|
function getDateTimeString() {
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
const day = String(now.getDate()).padStart(2, '0');
|
|
const hours = String(now.getHours()).padStart(2, '0');
|
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
}
|
|
|
|
/**
|
|
* Find files matching a pattern in a directory (cross-platform alternative to find)
|
|
* @param {string} dir - Directory to search
|
|
* @param {string} pattern - File pattern (e.g., "*.tmp", "*.md")
|
|
* @param {object} options - Options { maxAge: days, recursive: boolean }
|
|
*/
|
|
function findFiles(dir, pattern, options = {}) {
|
|
const { maxAge = null, recursive = false } = options;
|
|
const results = [];
|
|
|
|
if (!fs.existsSync(dir)) {
|
|
return results;
|
|
}
|
|
|
|
const regexPattern = pattern
|
|
.replace(/\./g, '\\.')
|
|
.replace(/\*/g, '.*')
|
|
.replace(/\?/g, '.');
|
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
|
|
function searchDir(currentDir) {
|
|
try {
|
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(currentDir, entry.name);
|
|
|
|
if (entry.isFile() && regex.test(entry.name)) {
|
|
if (maxAge !== null) {
|
|
const stats = fs.statSync(fullPath);
|
|
const ageInDays = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60 * 24);
|
|
if (ageInDays <= maxAge) {
|
|
results.push({ path: fullPath, mtime: stats.mtimeMs });
|
|
}
|
|
} else {
|
|
const stats = fs.statSync(fullPath);
|
|
results.push({ path: fullPath, mtime: stats.mtimeMs });
|
|
}
|
|
} else if (entry.isDirectory() && recursive) {
|
|
searchDir(fullPath);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// Ignore permission errors
|
|
}
|
|
}
|
|
|
|
searchDir(dir);
|
|
|
|
// Sort by modification time (newest first)
|
|
results.sort((a, b) => b.mtime - a.mtime);
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Read JSON from stdin (for hook input)
|
|
*/
|
|
async function readStdinJson() {
|
|
return new Promise((resolve, reject) => {
|
|
let data = '';
|
|
|
|
process.stdin.setEncoding('utf8');
|
|
process.stdin.on('data', chunk => {
|
|
data += chunk;
|
|
});
|
|
|
|
process.stdin.on('end', () => {
|
|
try {
|
|
if (data.trim()) {
|
|
resolve(JSON.parse(data));
|
|
} else {
|
|
resolve({});
|
|
}
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
|
|
process.stdin.on('error', reject);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Log to stderr (visible in Cursor hook output)
|
|
*/
|
|
function log(message) {
|
|
console.error(message);
|
|
}
|
|
|
|
/**
|
|
* Output to stdout (returned to Cursor)
|
|
*/
|
|
function output(data) {
|
|
if (typeof data === 'object') {
|
|
console.log(JSON.stringify(data));
|
|
} else {
|
|
console.log(data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read a text file safely
|
|
*/
|
|
function readFile(filePath) {
|
|
try {
|
|
return fs.readFileSync(filePath, 'utf8');
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write a text file
|
|
*/
|
|
function writeFile(filePath, content) {
|
|
ensureDir(path.dirname(filePath));
|
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
}
|
|
|
|
/**
|
|
* Append to a text file
|
|
*/
|
|
function appendFile(filePath, content) {
|
|
ensureDir(path.dirname(filePath));
|
|
fs.appendFileSync(filePath, content, 'utf8');
|
|
}
|
|
|
|
/**
|
|
* Check if a command exists in PATH
|
|
*/
|
|
function commandExists(cmd) {
|
|
try {
|
|
if (isWindows) {
|
|
execSync(`where ${cmd}`, { stdio: 'pipe' });
|
|
} else {
|
|
execSync(`which ${cmd}`, { stdio: 'pipe' });
|
|
}
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run a command and return output
|
|
*/
|
|
function runCommand(cmd, options = {}) {
|
|
try {
|
|
const result = execSync(cmd, {
|
|
encoding: 'utf8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
...options
|
|
});
|
|
return { success: true, output: result.trim() };
|
|
} catch (err) {
|
|
return { success: false, output: err.stderr || err.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if current directory is a git repository
|
|
*/
|
|
function isGitRepo() {
|
|
return runCommand('git rev-parse --git-dir').success;
|
|
}
|
|
|
|
/**
|
|
* Get git modified files
|
|
*/
|
|
function getGitModifiedFiles(patterns = []) {
|
|
if (!isGitRepo()) return [];
|
|
|
|
const result = runCommand('git diff --name-only HEAD');
|
|
if (!result.success) return [];
|
|
|
|
let files = result.output.split('\n').filter(Boolean);
|
|
|
|
if (patterns.length > 0) {
|
|
files = files.filter(file => {
|
|
return patterns.some(pattern => {
|
|
const regex = new RegExp(pattern);
|
|
return regex.test(file);
|
|
});
|
|
});
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
/**
|
|
* Replace text in a file (cross-platform sed alternative)
|
|
*/
|
|
function replaceInFile(filePath, search, replace) {
|
|
const content = readFile(filePath);
|
|
if (content === null) return false;
|
|
|
|
const newContent = content.replace(search, replace);
|
|
writeFile(filePath, newContent);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Count occurrences of a pattern in a file
|
|
*/
|
|
function countInFile(filePath, pattern) {
|
|
const content = readFile(filePath);
|
|
if (content === null) return 0;
|
|
|
|
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'g');
|
|
const matches = content.match(regex);
|
|
return matches ? matches.length : 0;
|
|
}
|
|
|
|
/**
|
|
* Search for pattern in file and return matching lines with line numbers
|
|
*/
|
|
function grepFile(filePath, pattern) {
|
|
const content = readFile(filePath);
|
|
if (content === null) return [];
|
|
|
|
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
const lines = content.split('\n');
|
|
const results = [];
|
|
|
|
lines.forEach((line, index) => {
|
|
if (regex.test(line)) {
|
|
results.push({ lineNumber: index + 1, content: line });
|
|
}
|
|
});
|
|
|
|
return results;
|
|
}
|
|
|
|
module.exports = {
|
|
// Platform info
|
|
isWindows,
|
|
isMacOS,
|
|
isLinux,
|
|
|
|
// Directories
|
|
getHomeDir,
|
|
getProjectRoot,
|
|
getCursorDir,
|
|
getClaudeDir,
|
|
getSessionsDir,
|
|
getLearnedSkillsDir,
|
|
getTempDir,
|
|
ensureDir,
|
|
|
|
// Date/Time
|
|
getDateString,
|
|
getTimeString,
|
|
getDateTimeString,
|
|
|
|
// File operations
|
|
findFiles,
|
|
readFile,
|
|
writeFile,
|
|
appendFile,
|
|
replaceInFile,
|
|
countInFile,
|
|
grepFile,
|
|
|
|
// Hook I/O
|
|
readStdinJson,
|
|
log,
|
|
output,
|
|
|
|
// System
|
|
commandExists,
|
|
runCommand,
|
|
isGitRepo,
|
|
getGitModifiedFiles
|
|
};
|