qraft
Version:
A powerful CLI tool to qraft structured project setups from GitHub template repositories
653 lines • 27.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.ManifestUtils = exports.MANIFEST_CONSTANTS = void 0;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
/**
* Constants for manifest storage
*/
exports.MANIFEST_CONSTANTS = {
QRAFT_DIR: '.qraft',
MANIFEST_FILE: 'manifest.json',
METADATA_FILE: 'metadata.json'
};
/**
* Utility functions for manifest path resolution and directory management
*/
class ManifestUtils {
/**
* Get the .qraft directory path for a target directory
* @param targetDirectory Target directory path
* @returns string Path to .qraft directory
*/
static getQraftDirectoryPath(targetDirectory) {
return path.join(targetDirectory, exports.MANIFEST_CONSTANTS.QRAFT_DIR);
}
/**
* Get the manifest.json file path for a target directory
* @param targetDirectory Target directory path
* @returns string Path to manifest.json file
*/
static getManifestFilePath(targetDirectory) {
return path.join(this.getQraftDirectoryPath(targetDirectory), exports.MANIFEST_CONSTANTS.MANIFEST_FILE);
}
/**
* Get the metadata.json file path for a target directory
* @param targetDirectory Target directory path
* @returns string Path to metadata.json file
*/
static getMetadataFilePath(targetDirectory) {
return path.join(this.getQraftDirectoryPath(targetDirectory), exports.MANIFEST_CONSTANTS.METADATA_FILE);
}
/**
* Ensure the .qraft directory exists in the target directory
* @param targetDirectory Target directory path
* @returns Promise<void>
*/
static async ensureQraftDirectory(targetDirectory) {
const qraftDir = this.getQraftDirectoryPath(targetDirectory);
await fs.ensureDir(qraftDir);
}
/**
* Check if .qraft directory exists in the target directory
* @param targetDirectory Target directory path
* @returns Promise<boolean> True if .qraft directory exists
*/
static async qraftDirectoryExists(targetDirectory) {
const qraftDir = this.getQraftDirectoryPath(targetDirectory);
return fs.pathExists(qraftDir);
}
/**
* Check if manifest.json exists in the target directory
* @param targetDirectory Target directory path
* @returns Promise<boolean> True if manifest.json exists
*/
static async manifestFileExists(targetDirectory) {
const manifestPath = this.getManifestFilePath(targetDirectory);
return fs.pathExists(manifestPath);
}
/**
* Check if metadata.json exists in the target directory
* @param targetDirectory Target directory path
* @returns Promise<boolean> True if metadata.json exists
*/
static async metadataFileExists(targetDirectory) {
const metadataPath = this.getMetadataFilePath(targetDirectory);
return fs.pathExists(metadataPath);
}
/**
* Check if both manifest and metadata files exist (complete local manifest)
* @param targetDirectory Target directory path
* @returns Promise<boolean> True if both files exist
*/
static async hasCompleteLocalManifest(targetDirectory) {
const [manifestExists, metadataExists] = await Promise.all([
this.manifestFileExists(targetDirectory),
this.metadataFileExists(targetDirectory)
]);
return manifestExists && metadataExists;
}
/**
* Validate manifest structure and required fields
* @param manifest Manifest object to validate
* @returns boolean True if manifest is valid
* @throws Error if manifest is invalid with specific error message
*/
static validateManifest(manifest) {
if (!manifest || typeof manifest !== 'object') {
throw new Error('Manifest must be a valid object');
}
const requiredFields = ['name', 'description', 'author', 'version'];
for (const field of requiredFields) {
if (!manifest[field] || typeof manifest[field] !== 'string' || manifest[field].trim() === '') {
throw new Error(`Manifest missing required field: ${field}`);
}
}
// Validate optional fields if present
if (manifest.defaultTarget !== undefined && typeof manifest.defaultTarget !== 'string') {
throw new Error('Manifest field "defaultTarget" must be a string');
}
if (manifest.tags !== undefined) {
if (!Array.isArray(manifest.tags)) {
throw new Error('Manifest field "tags" must be an array');
}
if (!manifest.tags.every((tag) => typeof tag === 'string')) {
throw new Error('All tags must be strings');
}
}
if (manifest.exclude !== undefined) {
if (!Array.isArray(manifest.exclude)) {
throw new Error('Manifest field "exclude" must be an array');
}
if (!manifest.exclude.every((pattern) => typeof pattern === 'string')) {
throw new Error('All exclude patterns must be strings');
}
}
if (manifest.postInstall !== undefined) {
if (!Array.isArray(manifest.postInstall)) {
throw new Error('Manifest field "postInstall" must be an array');
}
if (!manifest.postInstall.every((step) => typeof step === 'string')) {
throw new Error('All postInstall steps must be strings');
}
}
return true;
}
/**
* Read and parse manifest.json file with validation
* @param targetDirectory Target directory path
* @returns Promise<BoxManifest> Parsed and validated manifest
* @throws Error if file doesn't exist, can't be parsed, or is invalid
*/
static async readManifestFile(targetDirectory) {
const manifestPath = this.getManifestFilePath(targetDirectory);
if (!(await fs.pathExists(manifestPath))) {
throw new Error(`Manifest file not found: ${manifestPath}`);
}
try {
const content = await fs.readFile(manifestPath, 'utf-8');
// Check if file is empty
if (!content.trim()) {
throw new Error(`Manifest file is empty: ${manifestPath}`);
}
let manifest;
try {
manifest = JSON.parse(content);
}
catch (parseError) {
// Try to provide more helpful error information
const lines = content.split('\n');
const errorInfo = parseError instanceof Error ? parseError.message : String(parseError);
throw new Error(`Invalid JSON in manifest file ${manifestPath}: ${errorInfo}. File has ${lines.length} lines.`);
}
// Validate the manifest
try {
this.validateManifest(manifest);
}
catch (validationError) {
throw new Error(`Manifest validation failed for ${manifestPath}: ${validationError instanceof Error ? validationError.message : String(validationError)}`);
}
return manifest;
}
catch (error) {
if (error instanceof Error && error.message.includes('EACCES')) {
throw new Error(`Permission denied reading manifest file: ${manifestPath}`);
}
if (error instanceof Error && error.message.includes('ENOENT')) {
throw new Error(`Manifest file not found: ${manifestPath}`);
}
throw error;
}
}
/**
* Write manifest to manifest.json file with proper formatting
* @param targetDirectory Target directory path
* @param manifest Manifest to write
* @returns Promise<void>
*/
static async writeManifestFile(targetDirectory, manifest) {
// Validate before writing
this.validateManifest(manifest);
await this.ensureQraftDirectory(targetDirectory);
const manifestPath = this.getManifestFilePath(targetDirectory);
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
}
/**
* Read metadata.json file
* @param targetDirectory Target directory path
* @returns Promise<any> Parsed metadata object
* @throws Error if file doesn't exist or can't be parsed
*/
static async readMetadataFile(targetDirectory) {
const metadataPath = this.getMetadataFilePath(targetDirectory);
if (!(await fs.pathExists(metadataPath))) {
throw new Error(`Metadata file not found: ${metadataPath}`);
}
try {
const content = await fs.readFile(metadataPath, 'utf-8');
// Check if file is empty
if (!content.trim()) {
throw new Error(`Metadata file is empty: ${metadataPath}`);
}
try {
const metadata = JSON.parse(content);
// Basic validation of metadata structure
if (!metadata || typeof metadata !== 'object') {
throw new Error(`Metadata file contains invalid data: ${metadataPath}`);
}
return metadata;
}
catch (parseError) {
const lines = content.split('\n');
const errorInfo = parseError instanceof Error ? parseError.message : String(parseError);
throw new Error(`Invalid JSON in metadata file ${metadataPath}: ${errorInfo}. File has ${lines.length} lines.`);
}
}
catch (error) {
if (error instanceof Error && error.message.includes('EACCES')) {
throw new Error(`Permission denied reading metadata file: ${metadataPath}`);
}
if (error instanceof Error && error.message.includes('ENOENT')) {
throw new Error(`Metadata file not found: ${metadataPath}`);
}
throw error;
}
}
/**
* Write metadata to metadata.json file
* @param targetDirectory Target directory path
* @param metadata Metadata object to write
* @returns Promise<void>
*/
static async writeMetadataFile(targetDirectory, metadata) {
await this.ensureQraftDirectory(targetDirectory);
const metadataPath = this.getMetadataFilePath(targetDirectory);
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
}
/**
* Remove the entire .qraft directory and all its contents
* @param targetDirectory Target directory path
* @returns Promise<void>
*/
static async removeQraftDirectory(targetDirectory) {
const qraftDir = this.getQraftDirectoryPath(targetDirectory);
if (await fs.pathExists(qraftDir)) {
await fs.remove(qraftDir);
}
}
/**
* Get relative path from target directory to .qraft directory (for exclude patterns)
* @param targetDirectory Target directory path
* @returns string Relative path to .qraft directory
*/
static getQraftDirectoryRelativePath() {
return exports.MANIFEST_CONSTANTS.QRAFT_DIR;
}
/**
* Check if a path is within the .qraft directory
* @param filePath File path to check
* @returns boolean True if path is within .qraft directory
*/
static isQraftPath(filePath) {
const normalizedPath = path.normalize(filePath);
return normalizedPath.startsWith(exports.MANIFEST_CONSTANTS.QRAFT_DIR + path.sep) ||
normalizedPath === exports.MANIFEST_CONSTANTS.QRAFT_DIR;
}
/**
* Get default exclude patterns that should include .qraft directory
* @param existingExcludes Existing exclude patterns
* @returns string[] Updated exclude patterns including .qraft
*/
static getUpdatedExcludePatterns(existingExcludes = []) {
const qraftPattern = exports.MANIFEST_CONSTANTS.QRAFT_DIR + '/';
// Check if .qraft is already excluded
const hasQraftExclude = existingExcludes.some(pattern => pattern === exports.MANIFEST_CONSTANTS.QRAFT_DIR ||
pattern === qraftPattern ||
pattern.startsWith(exports.MANIFEST_CONSTANTS.QRAFT_DIR + '/'));
if (hasQraftExclude) {
return existingExcludes;
}
return [...existingExcludes, qraftPattern];
}
/**
* Find all directories containing .qraft manifests within a parent directory
* @param parentDirectory Parent directory to search
* @param maxDepth Maximum depth to search (default: 3)
* @returns Promise<string[]> Array of directories containing manifests
*/
static async findManifestDirectories(parentDirectory, maxDepth = 3) {
const manifestDirs = [];
async function searchDirectory(dir, currentDepth) {
if (currentDepth > maxDepth)
return;
try {
if (!(await fs.pathExists(dir)))
return;
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const fullPath = path.join(dir, entry.name);
// Check if this directory has a .qraft manifest
if (await ManifestUtils.hasCompleteLocalManifest(fullPath)) {
manifestDirs.push(fullPath);
}
// Recursively search subdirectories (but skip .qraft directories)
if (entry.name !== exports.MANIFEST_CONSTANTS.QRAFT_DIR) {
await searchDirectory(fullPath, currentDepth + 1);
}
}
}
}
catch (error) {
// Skip directories we can't read
}
}
await searchDirectory(parentDirectory, 0);
return manifestDirs.sort();
}
/**
* Get manifest directory info including size and file count
* @param targetDirectory Directory to analyze
* @returns Promise<ManifestDirectoryInfo | null> Directory info or null if no manifest
*/
static async getManifestDirectoryInfo(targetDirectory) {
if (!(await this.hasCompleteLocalManifest(targetDirectory))) {
return null;
}
const qraftDir = this.getQraftDirectoryPath(targetDirectory);
try {
const stats = await fs.stat(qraftDir);
const entries = await fs.readdir(qraftDir);
let totalSize = 0;
for (const entry of entries) {
const entryPath = path.join(qraftDir, entry);
const entryStat = await fs.stat(entryPath);
totalSize += entryStat.size;
}
return {
path: targetDirectory,
qraftPath: qraftDir,
fileCount: entries.length,
totalSize,
lastModified: stats.mtime,
hasManifest: await this.manifestFileExists(targetDirectory),
hasMetadata: await this.metadataFileExists(targetDirectory)
};
}
catch (error) {
return null;
}
}
/**
* Resolve relative paths within manifest context
* @param basePath Base directory path
* @param relativePath Relative path to resolve
* @returns string Resolved absolute path
*/
static resolveManifestPath(basePath, relativePath) {
if (path.isAbsolute(relativePath)) {
return relativePath;
}
return path.resolve(basePath, relativePath);
}
/**
* Get relative path from one directory to another
* @param from Source directory
* @param to Target directory
* @returns string Relative path
*/
static getRelativePath(from, to) {
return path.relative(from, to);
}
/**
* Normalize path separators for cross-platform compatibility
* @param filePath Path to normalize
* @returns string Normalized path
*/
static normalizePath(filePath) {
return path.normalize(filePath).replace(/\\/g, '/');
}
/**
* Check if a path is safe (doesn't escape parent directory)
* @param basePath Base directory
* @param targetPath Path to check
* @returns boolean True if path is safe
*/
static isSafePath(basePath, targetPath) {
const resolvedBase = path.resolve(basePath);
const resolvedTarget = path.resolve(basePath, targetPath);
return resolvedTarget.startsWith(resolvedBase);
}
/**
* Create backup of .qraft directory
* @param targetDirectory Directory containing .qraft
* @param backupSuffix Suffix for backup directory (default: timestamp)
* @returns Promise<string> Path to backup directory
*/
static async backupQraftDirectory(targetDirectory, backupSuffix) {
const qraftDir = this.getQraftDirectoryPath(targetDirectory);
if (!(await fs.pathExists(qraftDir))) {
throw new Error('No .qraft directory found to backup');
}
const suffix = backupSuffix || new Date().toISOString().replace(/[:.]/g, '-');
const backupDir = path.join(targetDirectory, `.qraft-backup-${suffix}`);
await fs.copy(qraftDir, backupDir);
return backupDir;
}
/**
* Restore .qraft directory from backup
* @param targetDirectory Directory to restore to
* @param backupPath Path to backup directory
* @returns Promise<void>
*/
static async restoreQraftDirectory(targetDirectory, backupPath) {
if (!(await fs.pathExists(backupPath))) {
throw new Error(`Backup directory not found: ${backupPath}`);
}
const qraftDir = this.getQraftDirectoryPath(targetDirectory);
// Remove existing .qraft directory if it exists
if (await fs.pathExists(qraftDir)) {
await fs.remove(qraftDir);
}
// Copy backup to .qraft directory
await fs.copy(backupPath, qraftDir);
}
/**
* Clean up old backup directories
* @param targetDirectory Directory to clean
* @param maxAge Maximum age in days (default: 30)
* @returns Promise<string[]> Array of removed backup paths
*/
static async cleanupOldBackups(targetDirectory, maxAge = 30) {
const removedBackups = [];
const maxAgeMs = maxAge * 24 * 60 * 60 * 1000;
const now = Date.now();
try {
const entries = await fs.readdir(targetDirectory, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith('.qraft-backup-')) {
const backupPath = path.join(targetDirectory, entry.name);
const stats = await fs.stat(backupPath);
if (now - stats.mtime.getTime() > maxAgeMs) {
await fs.remove(backupPath);
removedBackups.push(backupPath);
}
}
}
}
catch (error) {
// Ignore errors during cleanup
}
return removedBackups;
}
/**
* Get disk usage for .qraft directory
* @param targetDirectory Directory containing .qraft
* @returns Promise<DiskUsage> Disk usage information
*/
static async getQraftDiskUsage(targetDirectory) {
const qraftDir = this.getQraftDirectoryPath(targetDirectory);
if (!(await fs.pathExists(qraftDir))) {
return { totalSize: 0, fileCount: 0, directories: [] };
}
let totalSize = 0;
let fileCount = 0;
const directories = [];
async function calculateSize(dir) {
let dirSize = 0;
let dirFiles = 0;
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isFile()) {
const stats = await fs.stat(fullPath);
dirSize += stats.size;
dirFiles++;
}
else if (entry.isDirectory()) {
const subResult = await calculateSize(fullPath);
dirSize += subResult.size;
dirFiles += subResult.files;
}
}
}
catch (error) {
// Skip directories we can't read
}
return { size: dirSize, files: dirFiles };
}
const result = await calculateSize(qraftDir);
totalSize = result.size;
fileCount = result.files;
return { totalSize, fileCount, directories };
}
/**
* Validate that a directory is suitable for manifest storage
* @param targetDirectory Directory to validate
* @returns Promise<ValidationResult> Validation result with details
*/
static async validateDirectoryForManifest(targetDirectory) {
const issues = [];
const warnings = [];
try {
// Check if directory exists
if (!(await fs.pathExists(targetDirectory))) {
issues.push('Target directory does not exist');
return { isValid: false, issues, warnings };
}
// Check if it's actually a directory
const stats = await fs.stat(targetDirectory);
if (!stats.isDirectory()) {
issues.push('Target path is not a directory');
return { isValid: false, issues, warnings };
}
// Check write permissions
try {
const testFile = path.join(targetDirectory, '.qraft-test-write');
await fs.writeFile(testFile, 'test');
await fs.remove(testFile);
}
catch (error) {
issues.push('No write permission in target directory');
}
// Check if .qraft already exists
const qraftDir = this.getQraftDirectoryPath(targetDirectory);
if (await fs.pathExists(qraftDir)) {
warnings.push('.qraft directory already exists');
// Check if it contains valid manifest files
const hasManifest = await this.manifestFileExists(targetDirectory);
const hasMetadata = await this.metadataFileExists(targetDirectory);
if (hasManifest && hasMetadata) {
warnings.push('Valid manifest already exists - will be overwritten');
}
else if (hasManifest || hasMetadata) {
warnings.push('Incomplete manifest files found - will be replaced');
}
}
// Check for potential conflicts with common files
const commonFiles = ['manifest.json', 'package.json', '.git'];
for (const file of commonFiles) {
if (await fs.pathExists(path.join(targetDirectory, file))) {
if (file === 'manifest.json') {
warnings.push('Root manifest.json found - may cause confusion');
}
}
}
return {
isValid: issues.length === 0,
issues,
warnings
};
}
catch (error) {
issues.push(`Error validating directory: ${error instanceof Error ? error.message : 'Unknown error'}`);
return { isValid: false, issues, warnings };
}
}
/**
* Get manifest file paths for a given directory
* @param targetDirectory Target directory
* @returns ManifestPaths Object containing all relevant paths
*/
static getManifestPaths(targetDirectory) {
const qraftDir = this.getQraftDirectoryPath(targetDirectory);
return {
targetDirectory: path.resolve(targetDirectory),
qraftDirectory: qraftDir,
manifestFile: this.getManifestFilePath(targetDirectory),
metadataFile: this.getMetadataFilePath(targetDirectory),
relativePaths: {
qraftDirectory: this.getQraftDirectoryRelativePath(),
manifestFile: path.join(this.getQraftDirectoryRelativePath(), exports.MANIFEST_CONSTANTS.MANIFEST_FILE),
metadataFile: path.join(this.getQraftDirectoryRelativePath(), exports.MANIFEST_CONSTANTS.METADATA_FILE)
}
};
}
/**
* Check if a directory structure is compatible with manifest storage
* @param targetDirectory Directory to check
* @returns Promise<CompatibilityResult> Compatibility assessment
*/
static async checkManifestCompatibility(targetDirectory) {
const validation = await this.validateDirectoryForManifest(targetDirectory);
const hasExistingManifest = await this.hasCompleteLocalManifest(targetDirectory);
let compatibilityLevel;
const recommendations = [];
if (!validation.isValid) {
compatibilityLevel = 'incompatible';
recommendations.push(...validation.issues.map(issue => `Fix: ${issue}`));
}
else if (validation.warnings.length > 0) {
compatibilityLevel = 'warning';
recommendations.push(...validation.warnings.map(warning => `Consider: ${warning}`));
}
else {
compatibilityLevel = 'compatible';
}
if (hasExistingManifest) {
recommendations.push('Existing manifest will be preserved or updated');
}
return {
compatibilityLevel,
hasExistingManifest,
validation,
recommendations
};
}
}
exports.ManifestUtils = ManifestUtils;
//# sourceMappingURL=manifestUtils.js.map