tasq/node_modules/@claude-flow/shared/dist/hooks/safety/file-organization.js

328 lines
14 KiB
JavaScript

/**
* V3 File Organization Hook
*
* TypeScript conversion of V2 file-hook.sh.
* Enforces file organization, blocks writes to root folder,
* suggests proper directories, and recommends formatters.
*
* @module v3/shared/hooks/safety/file-organization
*/
import * as path from 'path';
import { HookEvent, HookPriority, } from '../types.js';
/**
* File type mapping to directories
*/
const FILE_TYPE_DIRECTORIES = [
// Test files (MUST come before source files - more specific patterns first)
{ pattern: /\.test\.(ts|tsx|js|jsx)$/, directories: ['tests/', '__tests__/', 'test/'], type: 'test file', blockRoot: true },
{ pattern: /\.spec\.(ts|tsx|js|jsx)$/, directories: ['tests/', '__tests__/', 'test/', 'spec/'], type: 'spec file', blockRoot: true },
{ pattern: /_test\.go$/, directories: ['tests/', 'test/'], type: 'Go test file', blockRoot: true },
{ pattern: /test_.*\.py$/, directories: ['tests/', 'test/'], type: 'Python test file', blockRoot: true },
{ pattern: /.*_test\.py$/, directories: ['tests/', 'test/'], type: 'Python test file', blockRoot: true },
// Source files
{ pattern: /\.(ts|tsx)$/, directories: ['src/', 'lib/'], type: 'TypeScript source', blockRoot: true },
{ pattern: /\.(js|jsx|mjs|cjs)$/, directories: ['src/', 'lib/', 'dist/'], type: 'JavaScript source', blockRoot: true },
{ pattern: /\.py$/, directories: ['src/', 'lib/', 'app/'], type: 'Python source', blockRoot: true },
{ pattern: /\.go$/, directories: ['cmd/', 'pkg/', 'internal/'], type: 'Go source', blockRoot: true },
{ pattern: /\.rs$/, directories: ['src/'], type: 'Rust source', blockRoot: true },
{ pattern: /\.java$/, directories: ['src/main/java/', 'src/'], type: 'Java source', blockRoot: true },
{ pattern: /\.rb$/, directories: ['lib/', 'app/'], type: 'Ruby source', blockRoot: true },
{ pattern: /\.php$/, directories: ['src/', 'app/'], type: 'PHP source', blockRoot: true },
{ pattern: /\.cs$/, directories: ['src/'], type: 'C# source', blockRoot: true },
{ pattern: /\.cpp?$/, directories: ['src/'], type: 'C/C++ source', blockRoot: true },
{ pattern: /\.swift$/, directories: ['Sources/'], type: 'Swift source', blockRoot: true },
{ pattern: /\.kt$/, directories: ['src/main/kotlin/', 'src/'], type: 'Kotlin source', blockRoot: true },
// Config files (usually allowed at root)
{ pattern: /\.(json|yaml|yml|toml)$/, directories: ['config/', './', 'configs/'], type: 'config file', blockRoot: false },
{ pattern: /\.(env|env\.[a-z]+)$/, directories: ['./'], type: 'environment file', blockRoot: false },
// Documentation
{ pattern: /\.md$/, directories: ['docs/', './'], type: 'Markdown documentation', blockRoot: false },
{ pattern: /\.rst$/, directories: ['docs/'], type: 'reStructuredText documentation', blockRoot: true },
{ pattern: /\.adoc$/, directories: ['docs/'], type: 'AsciiDoc documentation', blockRoot: true },
// Assets
{ pattern: /\.(css|scss|sass|less)$/, directories: ['styles/', 'src/styles/', 'assets/css/'], type: 'stylesheet', blockRoot: true },
{ pattern: /\.(png|jpg|jpeg|gif|svg|ico)$/, directories: ['assets/', 'public/', 'images/', 'static/'], type: 'image', blockRoot: true },
{ pattern: /\.(woff2?|ttf|otf|eot)$/, directories: ['assets/fonts/', 'fonts/', 'public/fonts/'], type: 'font', blockRoot: true },
// Scripts
{ pattern: /\.sh$/, directories: ['scripts/', 'bin/'], type: 'shell script', blockRoot: true },
{ pattern: /\.ps1$/, directories: ['scripts/', 'bin/'], type: 'PowerShell script', blockRoot: true },
// Data
{ pattern: /\.sql$/, directories: ['migrations/', 'db/', 'database/'], type: 'SQL file', blockRoot: true },
{ pattern: /\.csv$/, directories: ['data/', 'fixtures/', 'test/fixtures/'], type: 'CSV data', blockRoot: true },
];
/**
* Formatter recommendations by file extension
*/
const FORMATTERS = {
'.ts': { name: 'Prettier', command: 'prettier --write', configFile: '.prettierrc' },
'.tsx': { name: 'Prettier', command: 'prettier --write', configFile: '.prettierrc' },
'.js': { name: 'Prettier', command: 'prettier --write', configFile: '.prettierrc' },
'.jsx': { name: 'Prettier', command: 'prettier --write', configFile: '.prettierrc' },
'.json': { name: 'Prettier', command: 'prettier --write', configFile: '.prettierrc' },
'.md': { name: 'Prettier', command: 'prettier --write', configFile: '.prettierrc' },
'.yaml': { name: 'Prettier', command: 'prettier --write', configFile: '.prettierrc' },
'.yml': { name: 'Prettier', command: 'prettier --write', configFile: '.prettierrc' },
'.py': { name: 'Black', command: 'black', configFile: 'pyproject.toml' },
'.go': { name: 'gofmt', command: 'gofmt -w', configFile: undefined },
'.rs': { name: 'rustfmt', command: 'rustfmt', configFile: 'rustfmt.toml' },
'.java': { name: 'google-java-format', command: 'google-java-format -i', configFile: undefined },
'.rb': { name: 'RuboCop', command: 'rubocop -a', configFile: '.rubocop.yml' },
'.php': { name: 'PHP-CS-Fixer', command: 'php-cs-fixer fix', configFile: '.php-cs-fixer.php' },
'.cs': { name: 'dotnet-format', command: 'dotnet format', configFile: '.editorconfig' },
'.swift': { name: 'swift-format', command: 'swift-format -i', configFile: '.swift-format' },
'.kt': { name: 'ktlint', command: 'ktlint -F', configFile: '.editorconfig' },
'.css': { name: 'Prettier', command: 'prettier --write', configFile: '.prettierrc' },
'.scss': { name: 'Prettier', command: 'prettier --write', configFile: '.prettierrc' },
'.html': { name: 'Prettier', command: 'prettier --write', configFile: '.prettierrc' },
};
/**
* Linter recommendations by file extension
*/
const LINTERS = {
'.ts': { name: 'ESLint', command: 'eslint --fix', configFile: '.eslintrc' },
'.tsx': { name: 'ESLint', command: 'eslint --fix', configFile: '.eslintrc' },
'.js': { name: 'ESLint', command: 'eslint --fix', configFile: '.eslintrc' },
'.jsx': { name: 'ESLint', command: 'eslint --fix', configFile: '.eslintrc' },
'.py': { name: 'Pylint', command: 'pylint', configFile: 'pylintrc' },
'.go': { name: 'golangci-lint', command: 'golangci-lint run', configFile: '.golangci.yml' },
'.rs': { name: 'Clippy', command: 'cargo clippy', configFile: 'Cargo.toml' },
'.rb': { name: 'RuboCop', command: 'rubocop', configFile: '.rubocop.yml' },
'.php': { name: 'PHPStan', command: 'phpstan analyse', configFile: 'phpstan.neon' },
};
/**
* Naming convention checks
*/
const NAMING_CONVENTIONS = [
{ pattern: /^[a-z][a-z0-9-]*\.[a-z]+$/, convention: 'kebab-case', fileTypes: /\.(tsx?|jsx?|css|scss)$/ },
{ pattern: /^[a-z][a-z0-9_]*\.[a-z]+$/, convention: 'snake_case', fileTypes: /\.py$/ },
{ pattern: /^[a-z][a-z0-9_]*\.[a-z]+$/, convention: 'snake_case', fileTypes: /\.go$/ },
{ pattern: /^[A-Z][a-zA-Z0-9]*\.[a-z]+$/, convention: 'PascalCase', fileTypes: /\.(java|kt|cs)$/ },
];
/**
* File Organization Hook Manager
*/
export class FileOrganizationHook {
registry;
projectRoot = process.cwd();
constructor(registry) {
this.registry = registry;
this.registerHooks();
}
/**
* Register file organization hooks
*/
registerHooks() {
// Pre-edit hook
this.registry.register(HookEvent.PreEdit, this.analyzeFileOperation.bind(this), HookPriority.High, { name: 'file-organization:pre-edit' });
// Pre-write hook
this.registry.register(HookEvent.PreWrite, this.analyzeFileOperation.bind(this), HookPriority.High, { name: 'file-organization:pre-write' });
}
/**
* Analyze file operation for organization issues
*/
async analyzeFileOperation(context) {
const fileInfo = context.file;
if (!fileInfo) {
return this.createResult(false, []);
}
const filePath = fileInfo.path;
const fileName = path.basename(filePath);
const dirName = path.dirname(filePath);
const ext = path.extname(filePath);
const issues = [];
const warnings = [];
let blocked = false;
let blockReason;
let suggestedPath;
let suggestedDirectory;
// Check if writing to root folder
const isRootWrite = this.isRootDirectory(dirName);
const fileTypeInfo = this.getFileTypeInfo(fileName);
if (isRootWrite && fileTypeInfo?.blockRoot) {
blocked = true;
blockReason = `Source files should not be written to root folder. Suggested: ${fileTypeInfo.directories[0]}`;
suggestedDirectory = fileTypeInfo.directories[0];
suggestedPath = path.join(suggestedDirectory, fileName);
issues.push({
type: 'root-write',
severity: 'error',
description: `${fileTypeInfo.type} files should not be in the root directory`,
suggestedFix: `Move to ${suggestedDirectory}`,
});
}
// Check if file is in wrong directory
if (!isRootWrite && fileTypeInfo) {
const isInCorrectDir = fileTypeInfo.directories.some(dir => this.normalizePath(dirName).includes(this.normalizePath(dir)));
if (!isInCorrectDir) {
issues.push({
type: 'wrong-directory',
severity: 'warning',
description: `${fileTypeInfo.type} typically goes in: ${fileTypeInfo.directories.join(' or ')}`,
suggestedFix: `Consider moving to ${fileTypeInfo.directories[0]}`,
});
warnings.push(`File may be in wrong directory. Expected: ${fileTypeInfo.directories.join(' or ')}`);
}
}
// Check naming convention
const namingIssue = this.checkNamingConvention(fileName, ext);
if (namingIssue) {
issues.push(namingIssue);
warnings.push(namingIssue.description);
}
// Get formatter recommendation
const formatter = this.getFormatterRecommendation(ext);
// Get linter recommendation
const linter = this.getLinterRecommendation(ext);
return {
success: true,
blocked,
blockReason,
suggestedPath,
suggestedDirectory,
formatter,
linter,
fileType: fileTypeInfo?.type,
warnings: warnings.length > 0 ? warnings : undefined,
issues: issues.length > 0 ? issues : undefined,
abort: blocked,
data: blocked ? undefined : {
file: {
...fileInfo,
path: suggestedPath || filePath,
},
metadata: {
formatter: formatter?.command,
linter: linter?.command,
},
},
};
}
/**
* Check if directory is root
*/
isRootDirectory(dirName) {
const normalized = this.normalizePath(dirName);
return normalized === '.' ||
normalized === './' ||
normalized === '' ||
normalized === this.normalizePath(this.projectRoot);
}
/**
* Normalize path for comparison
*/
normalizePath(p) {
return p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '');
}
/**
* Get file type information
*/
getFileTypeInfo(fileName) {
for (const info of FILE_TYPE_DIRECTORIES) {
if (info.pattern.test(fileName)) {
return {
directories: info.directories,
type: info.type,
blockRoot: info.blockRoot,
};
}
}
return null;
}
/**
* Check naming convention
*/
checkNamingConvention(fileName, ext) {
for (const rule of NAMING_CONVENTIONS) {
if (rule.fileTypes.test(ext)) {
const baseName = fileName.replace(ext, '');
if (!rule.pattern.test(fileName)) {
return {
type: 'naming-convention',
severity: 'info',
description: `File name may not follow ${rule.convention} convention`,
suggestedFix: `Consider renaming to follow ${rule.convention}`,
};
}
}
}
return null;
}
/**
* Get formatter recommendation
*/
getFormatterRecommendation(ext) {
return FORMATTERS[ext];
}
/**
* Get linter recommendation
*/
getLinterRecommendation(ext) {
return LINTERS[ext];
}
/**
* Create result object
*/
createResult(blocked, issues) {
return {
success: true,
blocked,
issues: issues.length > 0 ? issues : undefined,
};
}
/**
* Manually analyze a file path
*/
async analyze(filePath) {
const context = {
event: HookEvent.PreEdit,
timestamp: new Date(),
file: {
path: filePath,
operation: 'write',
},
};
return this.analyzeFileOperation(context);
}
/**
* Get suggested directory for a file
*/
getSuggestedDirectory(fileName) {
const info = this.getFileTypeInfo(fileName);
return info?.directories[0] || null;
}
/**
* Check if a file path would be blocked
*/
wouldBlock(filePath) {
const fileName = path.basename(filePath);
const dirName = path.dirname(filePath);
const isRoot = this.isRootDirectory(dirName);
const info = this.getFileTypeInfo(fileName);
return isRoot && (info?.blockRoot ?? false);
}
/**
* Set project root directory
*/
setProjectRoot(root) {
this.projectRoot = root;
}
/**
* Get all formatter recommendations
*/
getAllFormatters() {
return { ...FORMATTERS };
}
/**
* Get all linter recommendations
*/
getAllLinters() {
return { ...LINTERS };
}
}
/**
* Create file organization hook
*/
export function createFileOrganizationHook(registry) {
return new FileOrganizationHook(registry);
}
//# sourceMappingURL=file-organization.js.map