qraft
Version:
A powerful CLI tool to qraft structured project setups from GitHub template repositories
795 lines • 29.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.QraftPatterns = exports.QraftPatternCategory = void 0;
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const config_1 = require("./config");
/**
* Categories of qraft-specific patterns
*/
var QraftPatternCategory;
(function (QraftPatternCategory) {
QraftPatternCategory["LOCAL"] = "local";
QraftPatternCategory["GLOBAL"] = "global";
QraftPatternCategory["CACHE"] = "cache";
QraftPatternCategory["CONFIG"] = "config";
})(QraftPatternCategory || (exports.QraftPatternCategory = QraftPatternCategory = {}));
/**
* QraftPatterns utility manages all qraft-specific gitignore patterns
*/
class QraftPatterns {
constructor(configManager) {
this.configManager = configManager || new config_1.ConfigManager();
}
/**
* Get all static qraft patterns that don't depend on configuration
* @returns QraftPattern[] Array of static patterns
*/
getStaticPatterns() {
return [
// Local qraft directory
{
pattern: '.qraft/',
category: QraftPatternCategory.LOCAL,
description: 'Qraft box metadata and sync information',
isStatic: true
},
// Local configuration files
{
pattern: '.qraftrc',
category: QraftPatternCategory.CONFIG,
description: 'Local qraft configuration file',
isStatic: true
},
{
pattern: '.qraftrc.json',
category: QraftPatternCategory.CONFIG,
description: 'Local qraft configuration file (JSON format)',
isStatic: true
},
{
pattern: '.qraftrc.yaml',
category: QraftPatternCategory.CONFIG,
description: 'Local qraft configuration file (YAML format)',
isStatic: true
},
{
pattern: '.qraftrc.yml',
category: QraftPatternCategory.CONFIG,
description: 'Local qraft configuration file (YAML format)',
isStatic: true
}
];
}
/**
* Get dynamic qraft patterns that depend on configuration
* @returns Promise<QraftPattern[]> Array of dynamic patterns
*/
async getDynamicPatterns() {
const patterns = [];
try {
const config = await this.configManager.getConfig();
// Add cache directory patterns
const cachePatterns = await this.getCachePatterns(config);
patterns.push(...cachePatterns);
// Add registry-specific patterns
const registryPatterns = await this.getRegistryPatterns(config);
patterns.push(...registryPatterns);
// Add authentication-related patterns
const authPatterns = await this.getAuthPatterns(config);
patterns.push(...authPatterns);
}
catch (error) {
// If config loading fails, just return empty array for dynamic patterns
// Static patterns will still be available
}
return patterns;
}
/**
* Get cache-related patterns from configuration
* @param config Qraft configuration
* @returns Promise<QraftPattern[]> Cache patterns
*/
async getCachePatterns(config) {
const patterns = [];
// Main cache directory
if (config.cache?.directory) {
const cacheDir = config.cache.directory;
// Only add if it's not the default system cache location
if (!this.isSystemCacheDirectory(cacheDir)) {
const relativePattern = this.getRelativePattern(cacheDir);
if (relativePattern) {
patterns.push({
pattern: relativePattern,
category: QraftPatternCategory.CACHE,
description: 'Qraft cache directory',
isStatic: false
});
}
}
}
// Temporary cache files
patterns.push({
pattern: '.qraft-cache/',
category: QraftPatternCategory.CACHE,
description: 'Temporary qraft cache directory',
isStatic: false
});
// Cache lock files
patterns.push({
pattern: '.qraft-cache.lock',
category: QraftPatternCategory.CACHE,
description: 'Qraft cache lock file',
isStatic: false
});
return patterns;
}
/**
* Get registry-related patterns from configuration
* @param config Qraft configuration
* @returns Promise<QraftPattern[]> Registry patterns
*/
async getRegistryPatterns(config) {
const patterns = [];
// Registry-specific temporary files
if (config.registries) {
patterns.push({
pattern: '.qraft-registry-*',
category: QraftPatternCategory.LOCAL,
description: 'Temporary registry files',
isStatic: false
});
}
// Registry authentication tokens (if stored locally)
patterns.push({
pattern: '.qraft-tokens',
category: QraftPatternCategory.CONFIG,
description: 'Local registry authentication tokens',
isStatic: false
});
return patterns;
}
/**
* Get authentication-related patterns from configuration
* @param _config Qraft configuration
* @returns Promise<QraftPattern[]> Authentication patterns
*/
async getAuthPatterns(_config) {
const patterns = [];
// GitHub token files
patterns.push({
pattern: '.qraft-github-token',
category: QraftPatternCategory.CONFIG,
description: 'Local GitHub authentication token',
isStatic: false
});
// SSH key files specific to qraft
patterns.push({
pattern: '.qraft-ssh-*',
category: QraftPatternCategory.CONFIG,
description: 'Qraft-specific SSH keys',
isStatic: false
});
return patterns;
}
/**
* Get configuration-specific patterns based on current config
* @returns Promise<QraftPattern[]> Configuration-specific patterns
*/
async getConfigSpecificPatterns() {
const patterns = [];
try {
const config = await this.configManager.getConfig();
// Add patterns based on enabled features
if (config.cache?.enabled) {
patterns.push({
pattern: '.qraft-temp-*',
category: QraftPatternCategory.CACHE,
description: 'Temporary qraft files',
isStatic: false
});
}
// Add patterns for each configured registry
if (config.registries) {
for (const registryName of Object.keys(config.registries)) {
const safeName = this.sanitizeRegistryName(registryName);
patterns.push({
pattern: `.qraft-${safeName}-*`,
category: QraftPatternCategory.LOCAL,
description: `Temporary files for ${registryName} registry`,
isStatic: false
});
}
}
}
catch (error) {
// If config loading fails, return empty array
}
return patterns;
}
/**
* Sanitize registry name for use in file patterns
* @param registryName Registry name to sanitize
* @returns string Sanitized name
*/
sanitizeRegistryName(registryName) {
return registryName
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
/**
* Get patterns based on current working directory and config
* @param targetDirectory Target directory for .gitignore
* @returns Promise<QraftPattern[]> Context-aware patterns
*/
async getContextAwarePatterns(targetDirectory) {
const allPatterns = await this.getAllPatterns();
const configSpecific = await this.getConfigSpecificPatterns();
const combined = [...allPatterns, ...configSpecific];
// Filter patterns based on directory context
const relevantPatterns = [];
for (const pattern of combined) {
if (await this.isPatternRelevantForContext(pattern, targetDirectory)) {
relevantPatterns.push(pattern);
}
}
return relevantPatterns;
}
/**
* Check if a pattern is relevant for the current context
* @param pattern Pattern to check
* @param targetDirectory Target directory
* @returns Promise<boolean> True if pattern is relevant
*/
async isPatternRelevantForContext(pattern, targetDirectory) {
// Always include local patterns
if (this.isLocalScopePattern(pattern)) {
return true;
}
// For cache patterns, check if cache is enabled and relevant
if (pattern.category === QraftPatternCategory.CACHE) {
try {
const config = await this.configManager.getConfig();
return config.cache?.enabled === true;
}
catch (error) {
return false;
}
}
// For config patterns, check if they could exist in target directory
if (pattern.category === QraftPatternCategory.CONFIG) {
return this.isConfigPatternRelevant(pattern.pattern, targetDirectory);
}
return this.isPatternRelevantForDirectory(pattern, targetDirectory);
}
/**
* Get all qraft patterns (static + dynamic)
* @returns Promise<QraftPattern[]> Array of all patterns
*/
async getAllPatterns() {
const staticPatterns = this.getStaticPatterns();
const dynamicPatterns = await this.getDynamicPatterns();
return [...staticPatterns, ...dynamicPatterns];
}
/**
* Get patterns organized by category
* @returns Promise<QraftPatternCollection> Patterns organized by category
*/
async getPatternsByCategory() {
const allPatterns = await this.getAllPatterns();
const collection = {
local: [],
global: [],
cache: [],
config: []
};
for (const pattern of allPatterns) {
collection[pattern.category].push(pattern);
}
return collection;
}
/**
* Get just the pattern strings (for use with GitignoreManager)
* @returns Promise<string[]> Array of pattern strings
*/
async getPatternStrings() {
const patterns = await this.getAllPatterns();
return patterns.map(p => p.pattern);
}
/**
* Get patterns for a specific category
* @param category Category to filter by
* @returns Promise<QraftPattern[]> Patterns in the specified category
*/
async getPatternsForCategory(category) {
const allPatterns = await this.getAllPatterns();
return allPatterns.filter(p => p.category === category);
}
/**
* Check if a directory is a system cache directory that shouldn't be ignored
* @param directory Directory path to check
* @returns boolean True if it's a system cache directory
*/
isSystemCacheDirectory(directory) {
const normalizedDir = path.normalize(directory);
const homeDir = os.homedir();
// Common system cache locations that we shouldn't ignore
const systemCachePaths = [
path.join(homeDir, '.cache'),
'/tmp',
'/var/tmp',
path.join(homeDir, 'Library', 'Caches'), // macOS
path.join(homeDir, 'AppData', 'Local'), // Windows
];
return systemCachePaths.some(systemPath => normalizedDir.startsWith(path.normalize(systemPath)));
}
/**
* Convert an absolute path to a relative pattern suitable for .gitignore
* @param absolutePath Absolute path to convert
* @returns string Relative pattern
*/
getRelativePattern(absolutePath) {
const cwd = process.cwd();
try {
const relativePath = path.relative(cwd, absolutePath);
// If the path is outside the current directory, don't create a pattern
if (relativePath.startsWith('..')) {
return '';
}
// Ensure directory patterns end with /
if (!relativePath.endsWith('/')) {
return relativePath + '/';
}
return relativePath;
}
catch (error) {
// If path.relative fails, return empty string
return '';
}
}
/**
* Get the default section title for qraft patterns
* @returns string Section title
*/
getSectionTitle() {
return 'Qraft CLI';
}
/**
* Get the default section description for qraft patterns
* @returns string Section description
*/
getSectionDescription() {
return 'Files and directories generated by qraft CLI tool';
}
/**
* Get patterns formatted for display (with descriptions)
* @returns Promise<string[]> Array of formatted pattern descriptions
*/
async getFormattedPatterns() {
const patterns = await this.getAllPatterns();
return patterns.map(pattern => `${pattern.pattern} - ${pattern.description}`);
}
/**
* Categorize patterns into local and global scopes
* @returns Promise<{local: QraftPattern[], global: QraftPattern[]}> Patterns categorized by scope
*/
async categorizeByScope() {
const patterns = await this.getAllPatterns();
const local = [];
const global = [];
for (const pattern of patterns) {
if (this.isLocalScopePattern(pattern)) {
local.push(pattern);
}
else {
global.push(pattern);
}
}
return { local, global };
}
/**
* Get only local-scope patterns (patterns that affect the current project)
* @returns Promise<QraftPattern[]> Local-scope patterns
*/
async getLocalPatterns() {
const { local } = await this.categorizeByScope();
return local;
}
/**
* Get only global-scope patterns (patterns that affect user's system)
* @returns Promise<QraftPattern[]> Global-scope patterns
*/
async getGlobalPatterns() {
const { global } = await this.categorizeByScope();
return global;
}
/**
* Determine if a pattern is local-scope (affects current project only)
* @param pattern Pattern to check
* @returns boolean True if pattern is local-scope
*/
isLocalScopePattern(pattern) {
// Local patterns are those that:
// 1. Are in the LOCAL category
// 2. Are relative paths within the project
// 3. Don't reference user home directory or system paths
if (pattern.category === QraftPatternCategory.LOCAL) {
return true;
}
// Config files in the project directory are local
if (pattern.category === QraftPatternCategory.CONFIG) {
return this.isProjectRelativePattern(pattern.pattern);
}
// Cache directories within the project are local
if (pattern.category === QraftPatternCategory.CACHE) {
return this.isProjectRelativePattern(pattern.pattern);
}
return false;
}
/**
* Check if a pattern is relative to the current project
* @param pattern Pattern to check
* @returns boolean True if pattern is project-relative
*/
isProjectRelativePattern(pattern) {
// Patterns starting with / are absolute
if (pattern.startsWith('/')) {
return false;
}
// Patterns starting with ~ reference home directory
if (pattern.startsWith('~')) {
return false;
}
// Patterns with .. go outside project directory
if (pattern.includes('..')) {
return false;
}
// Everything else is considered project-relative
return true;
}
/**
* Get patterns suitable for project-level .gitignore
* @returns Promise<string[]> Array of project-level pattern strings
*/
async getProjectPatterns() {
const localPatterns = await this.getLocalPatterns();
return localPatterns.map(p => p.pattern);
}
/**
* Get patterns that should be in global .gitignore
* @returns Promise<string[]> Array of global pattern strings
*/
async getGlobalGitignorePatterns() {
const globalPatterns = await this.getGlobalPatterns();
return globalPatterns.map(p => p.pattern);
}
/**
* Filter patterns based on current working directory context
* @param targetDirectory Directory where .gitignore will be created
* @returns Promise<QraftPattern[]> Patterns relevant to the target directory
*/
async getRelevantPatterns(targetDirectory) {
const allPatterns = await this.getAllPatterns();
const relevantPatterns = [];
for (const pattern of allPatterns) {
if (this.isPatternRelevantForDirectory(pattern, targetDirectory)) {
relevantPatterns.push(pattern);
}
}
return relevantPatterns;
}
/**
* Check if a pattern is relevant for a specific directory
* @param pattern Pattern to check
* @param targetDirectory Target directory path
* @returns boolean True if pattern is relevant
*/
isPatternRelevantForDirectory(pattern, targetDirectory) {
// Local patterns are always relevant
if (this.isLocalScopePattern(pattern)) {
return true;
}
// For global patterns, check if they would affect files in the target directory
if (pattern.category === QraftPatternCategory.CACHE) {
// Cache patterns are only relevant if the cache is within or affects the target directory
return this.isCachePatternRelevant(pattern.pattern, targetDirectory);
}
// Config patterns are relevant if they could exist in the target directory
if (pattern.category === QraftPatternCategory.CONFIG) {
return this.isConfigPatternRelevant(pattern.pattern, targetDirectory);
}
return false;
}
/**
* Check if a cache pattern is relevant for a directory
* @param pattern Cache pattern
* @param targetDirectory Target directory
* @returns boolean True if relevant
*/
isCachePatternRelevant(pattern, targetDirectory) {
// If pattern is relative, it's relevant
if (this.isProjectRelativePattern(pattern)) {
return true;
}
// If pattern is absolute, check if it's within the target directory tree
try {
const normalizedTarget = path.normalize(targetDirectory);
const normalizedPattern = path.normalize(pattern.replace(/\/$/, '')); // Remove trailing slash
return normalizedPattern.startsWith(normalizedTarget);
}
catch (error) {
return false;
}
}
/**
* Check if a config pattern is relevant for a directory
* @param pattern Config pattern
* @param _targetDirectory Target directory
* @returns boolean True if relevant
*/
isConfigPatternRelevant(pattern, _targetDirectory) {
// Config files are typically relevant for any project directory
// since they can be created in any project
return this.isProjectRelativePattern(pattern);
}
/**
* Validate a qraft pattern for correctness and safety
* @param pattern Pattern to validate
* @returns {isValid: boolean, errors: string[]} Validation result
*/
validatePattern(pattern) {
const errors = [];
// Check for empty pattern
if (!pattern || pattern.trim().length === 0) {
errors.push('Pattern cannot be empty');
return { isValid: false, errors };
}
const trimmedPattern = pattern.trim();
// Check for dangerous patterns
if (this.isDangerousPattern(trimmedPattern)) {
errors.push('Pattern is potentially dangerous and could ignore important files');
}
// Check for invalid characters
if (this.hasInvalidCharacters(trimmedPattern)) {
errors.push('Pattern contains invalid characters');
}
// Check for overly broad patterns
if (this.isOverlyBroadPattern(trimmedPattern)) {
errors.push('Pattern is too broad and may ignore unintended files');
}
// Check for malformed patterns
if (this.isMalformedPattern(trimmedPattern)) {
errors.push('Pattern is malformed');
}
return { isValid: errors.length === 0, errors };
}
/**
* Normalize a pattern to ensure consistency
* @param pattern Pattern to normalize
* @returns string Normalized pattern
*/
normalizePattern(pattern) {
if (!pattern) {
return '';
}
let normalized = pattern.trim();
// Remove redundant slashes
normalized = normalized.replace(/\/+/g, '/');
// Handle directory patterns consistently
if (this.isDirectoryPattern(normalized)) {
// Ensure directory patterns end with /
if (!normalized.endsWith('/')) {
normalized += '/';
}
}
// Remove leading ./ if present (except for negation patterns)
if (normalized.startsWith('./') && !normalized.startsWith('!')) {
normalized = normalized.slice(2);
}
// Normalize path separators for cross-platform compatibility
normalized = normalized.replace(/\\/g, '/');
return normalized;
}
/**
* Validate and normalize a collection of patterns
* @param patterns Array of patterns to process
* @returns {valid: string[], invalid: {pattern: string, errors: string[]}[]} Processed patterns
*/
validateAndNormalizePatterns(patterns) {
const valid = [];
const invalid = [];
for (const pattern of patterns) {
const validation = this.validatePattern(pattern);
if (validation.isValid) {
const normalized = this.normalizePattern(pattern);
if (normalized) {
valid.push(normalized);
}
}
else {
invalid.push({
pattern,
errors: validation.errors
});
}
}
return { valid, invalid };
}
/**
* Check if a pattern is dangerous (could ignore important files)
* @param pattern Pattern to check
* @returns boolean True if pattern is dangerous
*/
isDangerousPattern(pattern) {
const dangerousPatterns = [
'*', // Ignores everything
'**', // Ignores everything recursively
'/', // Ignores root
'.', // Ignores current directory
'..', // Ignores parent directory
'*.js', // Too broad for qraft
'*.ts', // Too broad for qraft
'*.json', // Too broad for qraft
'src/', // Common source directory
'lib/', // Common library directory
'dist/', // Build output (not qraft-specific)
'build/', // Build output (not qraft-specific)
];
return dangerousPatterns.includes(pattern.toLowerCase());
}
/**
* Check if a pattern contains invalid characters
* @param pattern Pattern to check
* @returns boolean True if pattern has invalid characters
*/
hasInvalidCharacters(pattern) {
// Check for null bytes and other control characters
if (pattern.includes('\0') || /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/.test(pattern)) {
return true;
}
// Check for Windows reserved characters in file names
if (/[<>:"|?*]/.test(pattern)) {
return true;
}
return false;
}
/**
* Check if a pattern is overly broad
* @param pattern Pattern to check
* @returns boolean True if pattern is too broad
*/
isOverlyBroadPattern(pattern) {
// Patterns that match too many files
const broadPatterns = [
/^\*$/, // Just *
/^\*\*$/, // Just **
/^\*\.\*$/, // *.*
/^[a-z]$/, // Single letter
/^[a-z]{1,2}\/$/, // Very short directory names
];
return broadPatterns.some(regex => regex.test(pattern));
}
/**
* Check if a pattern is malformed
* @param pattern Pattern to check
* @returns boolean True if pattern is malformed
*/
isMalformedPattern(pattern) {
// Check for unmatched brackets
const openBrackets = (pattern.match(/\[/g) || []).length;
const closeBrackets = (pattern.match(/\]/g) || []).length;
if (openBrackets !== closeBrackets) {
return true;
}
// Check for invalid escape sequences
if (/\\[^\\\/\[\]{}()*+?.^$|]/.test(pattern)) {
return true;
}
// Check for patterns ending with backslash
if (pattern.endsWith('\\')) {
return true;
}
return false;
}
/**
* Check if a pattern represents a directory
* @param pattern Pattern to check
* @returns boolean True if pattern is for a directory
*/
isDirectoryPattern(pattern) {
// Directory patterns typically end with / or are known directory names
if (pattern.endsWith('/')) {
return true;
}
// Known qraft directory patterns
const qraftDirectories = [
'.qraft',
'.qraft-cache',
'.qraft-temp',
];
return qraftDirectories.some(dir => pattern === dir || pattern.startsWith(dir + '-'));
}
/**
* Get validation rules for qraft patterns
* @returns object Validation rules and descriptions
*/
getValidationRules() {
return {
rules: [
{
name: 'Non-empty',
description: 'Patterns must not be empty or whitespace-only'
},
{
name: 'Safe patterns',
description: 'Patterns must not ignore important project files'
},
{
name: 'Valid characters',
description: 'Patterns must not contain control characters or reserved symbols'
},
{
name: 'Appropriate scope',
description: 'Patterns should be specific to qraft-generated files'
},
{
name: 'Well-formed',
description: 'Patterns must have valid gitignore syntax'
}
],
examples: {
valid: [
'.qraft/',
'.qraftrc',
'.qraft-cache/',
'.qraft-temp-*',
'!.qraft/important.json'
],
invalid: [
'*',
'*.js',
'',
'src/',
'pattern\0with\0nulls'
]
}
};
}
}
exports.QraftPatterns = QraftPatterns;
//# sourceMappingURL=qraftPatterns.js.map