qraft
Version:
A powerful CLI tool to qraft structured project setups from GitHub template repositories
446 lines • 17.5 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.GitignoreManager = exports.GitignoreFileError = exports.GitignorePermissionError = exports.GitignoreError = void 0;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
/**
* Custom error types for GitignoreManager operations
*/
class GitignoreError extends Error {
constructor(message, code, cause) {
super(message);
this.code = code;
this.cause = cause;
this.name = 'GitignoreError';
}
}
exports.GitignoreError = GitignoreError;
class GitignorePermissionError extends GitignoreError {
constructor(message, cause) {
super(message, 'PERMISSION_ERROR', cause);
this.name = 'GitignorePermissionError';
}
}
exports.GitignorePermissionError = GitignorePermissionError;
class GitignoreFileError extends GitignoreError {
constructor(message, cause) {
super(message, 'FILE_ERROR', cause);
this.name = 'GitignoreFileError';
}
}
exports.GitignoreFileError = GitignoreFileError;
/**
* GitignoreManager handles reading, writing, and managing .gitignore files
*/
class GitignoreManager {
constructor() {
this.gitignoreFileName = '.gitignore';
}
/**
* Handle and classify file system errors
* @param error Original error
* @param operation Description of the operation that failed
* @param filePath Optional file path for context
* @returns GitignoreError Classified error
*/
handleFileSystemError(error, operation, filePath) {
const baseMessage = filePath ? `${operation} for ${filePath}` : operation;
if (error instanceof Error) {
// Check for specific error codes
const nodeError = error;
switch (nodeError.code) {
case 'ENOENT':
return new GitignoreFileError(`File or directory not found: ${baseMessage}`, error);
case 'EACCES':
case 'EPERM':
return new GitignorePermissionError(`Permission denied: ${baseMessage}`, error);
case 'ENOTDIR':
return new GitignoreFileError(`Not a directory: ${baseMessage}`, error);
case 'EISDIR':
return new GitignoreFileError(`Is a directory: ${baseMessage}`, error);
case 'ENOSPC':
return new GitignoreFileError(`No space left on device: ${baseMessage}`, error);
case 'EROFS':
return new GitignoreFileError(`Read-only file system: ${baseMessage}`, error);
default:
return new GitignoreFileError(`${baseMessage}: ${error.message}`, error);
}
}
return new GitignoreError(`${baseMessage}: Unknown error`, 'UNKNOWN_ERROR');
}
/**
* Safely execute a file system operation with error handling
* @param operation Function to execute
* @param operationName Description of the operation
* @param filePath Optional file path for context
* @returns Promise<T> Result of the operation
*/
async safeFileOperation(operation, operationName, filePath) {
try {
return await operation();
}
catch (error) {
throw this.handleFileSystemError(error, operationName, filePath);
}
}
/**
* Get the path to the .gitignore file in the specified directory
* @param targetDirectory Directory containing the .gitignore file
* @returns string Path to .gitignore file
*/
getGitignorePath(targetDirectory) {
return path.join(targetDirectory, this.gitignoreFileName);
}
/**
* Check if a .gitignore file exists in the target directory
* @param targetDirectory Directory to check
* @returns Promise<boolean> True if .gitignore exists
*/
async exists(targetDirectory) {
const gitignorePath = this.getGitignorePath(targetDirectory);
try {
return await fs.pathExists(gitignorePath);
}
catch (error) {
return false;
}
}
/**
* Read the contents of a .gitignore file
* @param targetDirectory Directory containing the .gitignore file
* @returns Promise<string> Contents of the .gitignore file, empty string if file doesn't exist
*/
async read(targetDirectory) {
const gitignorePath = this.getGitignorePath(targetDirectory);
return this.safeFileOperation(async () => {
// Check if directory exists first
if (!(await fs.pathExists(targetDirectory))) {
throw new Error(`Directory does not exist: ${targetDirectory}`);
}
if (await this.exists(targetDirectory)) {
return await fs.readFile(gitignorePath, 'utf-8');
}
return '';
}, 'Reading .gitignore file', gitignorePath);
}
/**
* Write content to a .gitignore file
* @param targetDirectory Directory to write the .gitignore file
* @param content Content to write
* @param options Operation options
* @returns Promise<void>
*/
async write(targetDirectory, content, options = {}) {
const gitignorePath = this.getGitignorePath(targetDirectory);
if (options.dryRun) {
return; // Don't actually write in dry run mode
}
return this.safeFileOperation(async () => {
// Ensure target directory exists
await fs.ensureDir(targetDirectory);
// Write the file
await fs.writeFile(gitignorePath, content, 'utf-8');
}, 'Writing .gitignore file', gitignorePath);
}
/**
* Check if the target directory is writable
* @param targetDirectory Directory to check
* @returns Promise<boolean> True if directory is writable
*/
async isWritable(targetDirectory) {
try {
await fs.access(targetDirectory, fs.constants.W_OK);
return true;
}
catch (error) {
return false;
}
}
/**
* Check if a .gitignore file is writable (if it exists)
* @param targetDirectory Directory containing the .gitignore file
* @returns Promise<boolean> True if file is writable or doesn't exist
*/
async isGitignoreWritable(targetDirectory) {
const gitignorePath = this.getGitignorePath(targetDirectory);
try {
if (await fs.pathExists(gitignorePath)) {
await fs.access(gitignorePath, fs.constants.W_OK);
}
return true;
}
catch (error) {
return false;
}
}
/**
* Check if we have permission to create a .gitignore file
* @param targetDirectory Directory where .gitignore would be created
* @returns Promise<boolean> True if file can be created
*/
async canCreateGitignore(targetDirectory) {
try {
// Check if directory exists and is writable
if (!(await fs.pathExists(targetDirectory))) {
// Try to create the directory to test permissions
await fs.ensureDir(targetDirectory);
}
// Test write permission by checking parent directory if needed
const dirToCheck = await fs.pathExists(targetDirectory) ? targetDirectory : path.dirname(targetDirectory);
return await this.isWritable(dirToCheck);
}
catch (error) {
return false;
}
}
/**
* Perform comprehensive permission checks
* @param targetDirectory Directory to check
* @returns Promise<object> Object with permission check results
*/
async checkPermissions(targetDirectory) {
const result = {
canWrite: false,
fileExists: false,
fileWritable: false,
canCreate: false
};
try {
result.fileExists = await this.exists(targetDirectory);
result.canWrite = await this.isWritable(targetDirectory);
result.fileWritable = await this.isGitignoreWritable(targetDirectory);
result.canCreate = await this.canCreateGitignore(targetDirectory);
return result;
}
catch (error) {
result.error = error instanceof Error ? error.message : 'Unknown permission error';
return result;
}
}
/**
* Ensure the target directory exists and is writable
* @param targetDirectory Directory to validate
* @returns Promise<void>
* @throws Error if directory cannot be created or is not writable
*/
async validateTargetDirectory(targetDirectory) {
const permissions = await this.checkPermissions(targetDirectory);
if (permissions.error) {
throw new Error(`Permission check failed: ${permissions.error}`);
}
if (!permissions.canWrite) {
throw new Error(`Directory is not writable: ${targetDirectory}`);
}
if (permissions.fileExists && !permissions.fileWritable) {
throw new Error(`Existing .gitignore file is not writable: ${this.getGitignorePath(targetDirectory)}`);
}
if (!permissions.fileExists && !permissions.canCreate) {
throw new Error(`Cannot create .gitignore file in directory: ${targetDirectory}`);
}
}
/**
* Parse .gitignore content into individual patterns
* @param content Raw .gitignore file content
* @returns string[] Array of patterns (excluding comments and empty lines)
*/
parsePatterns(content) {
return content
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0 && !line.startsWith('#'));
}
/**
* Normalize a gitignore pattern for comparison
* @param pattern Raw pattern from .gitignore
* @returns string Normalized pattern
*/
normalizePattern(pattern) {
// Remove leading/trailing whitespace
let normalized = pattern.trim();
// Handle negation patterns
if (normalized.startsWith('!')) {
return normalized;
}
// Normalize directory patterns
if (normalized.endsWith('/') && !normalized.endsWith('*/')) {
normalized = normalized.slice(0, -1);
}
// Remove leading ./ if present
if (normalized.startsWith('./')) {
normalized = normalized.slice(2);
}
return normalized;
}
/**
* Check if a pattern already exists in the .gitignore content
* @param content Current .gitignore content
* @param pattern Pattern to check for
* @returns boolean True if pattern exists
*/
hasPattern(content, pattern) {
const existingPatterns = this.parsePatterns(content);
const normalizedPattern = this.normalizePattern(pattern);
return existingPatterns.some(existingPattern => this.normalizePattern(existingPattern) === normalizedPattern);
}
/**
* Filter out patterns that already exist in the .gitignore file
* @param content Current .gitignore content
* @param patterns Array of patterns to check
* @returns object Object with newPatterns and existingPatterns arrays
*/
filterDuplicatePatterns(content, patterns) {
const newPatterns = [];
const existingPatterns = [];
for (const pattern of patterns) {
if (this.hasPattern(content, pattern)) {
existingPatterns.push(pattern);
}
else {
newPatterns.push(pattern);
}
}
return { newPatterns, existingPatterns };
}
/**
* Format patterns into a properly commented section
* @param patterns Array of patterns to format
* @param sectionTitle Title for the section
* @param description Optional description for the section
* @returns string Formatted section with comments and patterns
*/
formatPatternSection(patterns, sectionTitle, description) {
if (patterns.length === 0) {
return '';
}
const lines = [];
// Add section header
lines.push(`# ${sectionTitle}`);
// Add description if provided
if (description) {
lines.push(`# ${description}`);
}
// Add patterns
patterns.forEach(pattern => {
lines.push(pattern);
});
return lines.join('\n');
}
/**
* Insert patterns into existing .gitignore content
* @param existingContent Current .gitignore content
* @param patterns Array of patterns to insert
* @param sectionTitle Title for the new section
* @param description Optional description for the section
* @returns string Updated .gitignore content
*/
insertPatterns(existingContent, patterns, sectionTitle, description) {
if (patterns.length === 0) {
return existingContent;
}
const formattedSection = this.formatPatternSection(patterns, sectionTitle, description);
// If file is empty or doesn't exist, just return the formatted section
if (!existingContent.trim()) {
return formattedSection;
}
// Ensure existing content ends with a newline
let content = existingContent;
if (!content.endsWith('\n')) {
content += '\n';
}
// Add a separator line if content doesn't already end with empty line
if (!content.endsWith('\n\n')) {
content += '\n';
}
// Append the new section with trailing newline
content += formattedSection + '\n';
return content;
}
/**
* Create or update .gitignore file with new patterns
* @param targetDirectory Directory containing the .gitignore file
* @param patterns Array of patterns to add
* @param sectionTitle Title for the section
* @param description Optional description for the section
* @param options Operation options
* @returns Promise<GitignoreOperationResult> Result of the operation
*/
async addPatterns(targetDirectory, patterns, sectionTitle, description, options = {}) {
const result = {
success: false,
created: false,
modified: false,
patternsAdded: [],
patternsSkipped: []
};
try {
// Validate target directory
await this.validateTargetDirectory(targetDirectory);
// Read existing content
const existingContent = await this.read(targetDirectory);
const fileExists = await this.exists(targetDirectory);
// Filter out duplicate patterns
const { newPatterns, existingPatterns } = this.filterDuplicatePatterns(existingContent, patterns);
result.patternsSkipped = existingPatterns;
// If no new patterns to add, return early
if (newPatterns.length === 0) {
result.success = true;
return result;
}
// Insert new patterns
const updatedContent = this.insertPatterns(existingContent, newPatterns, sectionTitle, description);
// Write updated content
await this.write(targetDirectory, updatedContent, options);
result.success = true;
result.created = !fileExists;
result.modified = fileExists;
result.patternsAdded = newPatterns;
return result;
}
catch (error) {
if (error instanceof GitignoreError) {
result.error = error.message;
result.errorCode = error.code;
}
else {
result.error = error instanceof Error ? error.message : 'Unknown error';
result.errorCode = 'UNKNOWN_ERROR';
}
return result;
}
}
}
exports.GitignoreManager = GitignoreManager;
//# sourceMappingURL=gitignoreManager.js.map