qraft
Version:
A powerful CLI tool to qraft structured project setups from GitHub template repositories
342 lines • 14.7 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.DirectoryScanner = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
class DirectoryScanner {
constructor() {
this.defaultExcludePatterns = {
directories: [
// Version control
'.git', '.svn', '.hg', '.bzr',
// Dependencies
'node_modules', 'vendor', '__pycache__', '.venv', 'venv', 'env',
// Build outputs
'dist', 'build', 'out', 'target', 'bin', 'obj',
// Framework specific
'.next', '.nuxt', '.vuepress', '.docusaurus',
// Testing and coverage
'coverage', '.nyc_output', '.pytest_cache', '__tests__/__snapshots__',
// Caches
'.cache', '.parcel-cache', '.webpack', '.rollup.cache',
// Temporary
'tmp', 'temp', '.tmp',
// IDE
'.vscode', '.idea', '.vs',
// OS
'.DS_Store', 'Thumbs.db'
],
files: [
// Logs
'*.log', '*.log.*', 'npm-debug.log*', 'yarn-debug.log*', 'yarn-error.log*',
// OS files
'.DS_Store', 'Thumbs.db', 'desktop.ini',
// Editor files
'*~', '*.swp', '*.swo', '.#*',
// Build artifacts
'*.pyc', '*.pyo', '*.class', '*.o', '*.so', '*.dll', '*.exe',
// Package files
'*.tar.gz', '*.zip', '*.rar', '*.7z',
// Lock files (optional - might want to include these)
// 'package-lock.json', 'yarn.lock', 'Pipfile.lock'
],
extensions: [
'.tmp', '.temp', '.bak', '.backup', '.old', '.orig'
],
patterns: [
// Backup files
'*~', '*.bak', '*.backup', '*.old', '*.orig',
// Temporary files
'*.tmp', '*.temp', '#*#', '.#*',
// Compiled files
'*.pyc', '*.pyo', '*.class', '*.o', '*.so'
]
};
this.defaultOptions = {
includeContent: false,
maxContentSize: 1024 * 1024, // 1MB
maxDepth: 10,
followSymlinks: false,
excludePatterns: this.flattenExclusionPatterns(this.defaultExcludePatterns),
includeHidden: false
};
}
async scanDirectory(directoryPath, options = {}) {
const opts = { ...this.defaultOptions, ...options };
const resolvedPath = path.resolve(directoryPath);
// Validate directory exists and is accessible
await this.validateDirectory(resolvedPath);
const structure = {
files: [],
directories: [],
totalFiles: 0,
totalDirectories: 0,
totalSize: 0,
depth: 0,
rootPath: resolvedPath
};
await this.scanRecursive(resolvedPath, resolvedPath, structure, opts, 0);
return structure;
}
async validateDirectory(directoryPath) {
try {
const stats = await fs.promises.stat(directoryPath);
if (!stats.isDirectory()) {
throw new Error(`Path is not a directory: ${directoryPath}`);
}
// Check if directory is readable
await fs.promises.access(directoryPath, fs.constants.R_OK);
}
catch (error) {
if (error instanceof Error) {
throw new Error(`Cannot access directory: ${error.message}`);
}
throw new Error(`Cannot access directory: ${directoryPath}`);
}
}
async scanRecursive(currentPath, rootPath, structure, options, currentDepth) {
if (currentDepth > options.maxDepth) {
return;
}
structure.depth = Math.max(structure.depth, currentDepth);
try {
const entries = await fs.promises.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name);
const relativePath = path.relative(rootPath, fullPath);
// Skip if excluded
if (this.shouldExclude(entry.name, relativePath, options)) {
continue;
}
// Handle symlinks
if (entry.isSymbolicLink() && !options.followSymlinks) {
continue;
}
const stats = await fs.promises.stat(fullPath);
const fileInfo = {
path: fullPath,
relativePath,
name: entry.name,
extension: path.extname(entry.name).toLowerCase(),
size: stats.size,
isDirectory: stats.isDirectory(),
isFile: stats.isFile(),
lastModified: stats.mtime
};
if (stats.isDirectory()) {
structure.directories.push(fileInfo);
structure.totalDirectories++;
// Recursively scan subdirectory
await this.scanRecursive(fullPath, rootPath, structure, options, currentDepth + 1);
}
else if (stats.isFile()) {
// Add content for small text files if requested
if (options.includeContent && this.shouldIncludeContent(fileInfo, options)) {
try {
fileInfo.content = await fs.promises.readFile(fullPath, 'utf-8');
}
catch {
// If we can't read as text, skip content
fileInfo.content = undefined;
}
}
structure.files.push(fileInfo);
structure.totalFiles++;
structure.totalSize += stats.size;
}
}
}
catch (error) {
// Log error but continue scanning other directories
console.warn(`Warning: Could not scan directory ${currentPath}:`, error);
}
}
flattenExclusionPatterns(patterns) {
return [
...patterns.directories,
...patterns.files,
...patterns.extensions,
...patterns.patterns
];
}
shouldExclude(fileName, relativePath, options) {
// Skip hidden files unless explicitly included
if (!options.includeHidden && fileName.startsWith('.')) {
// Allow some important hidden files for analysis (but they may still be flagged as sensitive)
const allowedHiddenFiles = ['.gitignore', '.gitattributes', '.editorconfig', '.env.example', '.env', '.env.local', '.env.production'];
if (!allowedHiddenFiles.includes(fileName)) {
return true;
}
}
// Check against comprehensive exclusion patterns
if (this.isExcludedByPatterns(fileName, relativePath, this.defaultExcludePatterns)) {
return true;
}
// Check against user-provided exclude patterns
for (const pattern of options.excludePatterns) {
if (this.matchesPattern(fileName, pattern) || this.matchesPattern(relativePath, pattern)) {
return true;
}
}
return false;
}
isExcludedByPatterns(fileName, relativePath, patterns) {
// Check directory exclusions
const pathParts = relativePath.split('/');
for (const part of pathParts) {
if (patterns.directories.includes(part)) {
return true;
}
}
// Check file exclusions
if (patterns.files.some(pattern => this.matchesPattern(fileName, pattern))) {
return true;
}
// Check extension exclusions
const extension = fileName.substring(fileName.lastIndexOf('.'));
if (patterns.extensions.includes(extension)) {
return true;
}
// Check pattern exclusions
if (patterns.patterns.some(pattern => this.matchesPattern(fileName, pattern))) {
return true;
}
return false;
}
matchesPattern(text, pattern) {
// Simple glob-like pattern matching
if (pattern.includes('*')) {
const regexPattern = pattern
.replace(/\./g, '\\.')
.replace(/\*/g, '.*');
return new RegExp(`^${regexPattern}$`).test(text);
}
return text === pattern || text.includes(pattern);
}
shouldIncludeContent(fileInfo, options) {
// Only include content for small text files
if (fileInfo.size > options.maxContentSize) {
return false;
}
// Check if it's likely a text file based on extension
const textExtensions = [
'.txt', '.md', '.json', '.js', '.ts', '.jsx', '.tsx', '.py', '.java',
'.c', '.cpp', '.h', '.hpp', '.cs', '.php', '.rb', '.go', '.rs',
'.html', '.css', '.scss', '.sass', '.less', '.xml', '.yaml', '.yml',
'.toml', '.ini', '.cfg', '.conf', '.sh', '.bat', '.ps1', '.sql',
'.dockerfile', '.gitignore', '.gitattributes', '.editorconfig'
];
return textExtensions.includes(fileInfo.extension) || !fileInfo.extension;
}
// Utility method to get directory summary
getDirectorySummary(structure) {
const { totalFiles, totalDirectories, totalSize, depth } = structure;
const sizeInMB = (totalSize / (1024 * 1024)).toFixed(2);
return `${totalFiles} files, ${totalDirectories} directories, ${sizeInMB}MB, depth: ${depth}`;
}
// Get file type distribution
getFileTypeDistribution(structure) {
const distribution = {};
for (const file of structure.files) {
const ext = file.extension || 'no-extension';
distribution[ext] = (distribution[ext] || 0) + 1;
}
return distribution;
}
// Get exclusion patterns for a specific category
getExclusionPatterns(category) {
if (category) {
return this.defaultExcludePatterns[category];
}
return this.flattenExclusionPatterns(this.defaultExcludePatterns);
}
// Check if a path would be excluded
wouldBeExcluded(filePath, options = {}) {
const opts = { ...this.defaultOptions, ...options };
const fileName = filePath.split('/').pop() || '';
return this.shouldExclude(fileName, filePath, opts);
}
// Get exclusion statistics for a directory
async getExclusionStats(directoryPath) {
const fs = require('fs');
const path = require('path');
let totalItems = 0;
let excludedItems = 0;
const exclusionReasons = {};
const scanForStats = async (currentPath, relativePath = '') => {
try {
const entries = await fs.promises.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
totalItems++;
const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
if (this.shouldExclude(entry.name, entryRelativePath, this.defaultOptions)) {
excludedItems++;
// Determine exclusion reason
if (entry.name.startsWith('.') && !this.defaultOptions.includeHidden) {
exclusionReasons['hidden files'] = (exclusionReasons['hidden files'] || 0) + 1;
}
else if (this.defaultExcludePatterns.directories.includes(entry.name)) {
exclusionReasons['excluded directories'] = (exclusionReasons['excluded directories'] || 0) + 1;
}
else if (this.defaultExcludePatterns.files.some(pattern => this.matchesPattern(entry.name, pattern))) {
exclusionReasons['excluded files'] = (exclusionReasons['excluded files'] || 0) + 1;
}
else {
exclusionReasons['pattern matches'] = (exclusionReasons['pattern matches'] || 0) + 1;
}
}
else if (entry.isDirectory()) {
// Recursively scan non-excluded directories
const fullPath = path.join(currentPath, entry.name);
await scanForStats(fullPath, entryRelativePath);
}
}
}
catch {
// Ignore errors and continue
}
};
await scanForStats(path.resolve(directoryPath));
return {
totalItems,
excludedItems,
includedItems: totalItems - excludedItems,
exclusionReasons
};
}
}
exports.DirectoryScanner = DirectoryScanner;
//# sourceMappingURL=directoryScanner.js.map