qraft
Version:
A powerful CLI tool to qraft structured project setups from GitHub template repositories
686 lines • 29.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.ManifestManager = exports.ManifestPermissionError = exports.ManifestValidationError = exports.ManifestNotFoundError = exports.ManifestCorruptionError = exports.ManifestError = void 0;
const crypto = __importStar(require("crypto"));
const manifestUtils_1 = require("../utils/manifestUtils");
/**
* Custom error types for manifest operations
*/
class ManifestError extends Error {
constructor(message, code, cause) {
super(message);
this.code = code;
this.cause = cause;
this.name = 'ManifestError';
}
}
exports.ManifestError = ManifestError;
class ManifestCorruptionError extends ManifestError {
constructor(message, cause) {
super(message, 'MANIFEST_CORRUPTED', cause);
this.name = 'ManifestCorruptionError';
}
}
exports.ManifestCorruptionError = ManifestCorruptionError;
class ManifestNotFoundError extends ManifestError {
constructor(message, cause) {
super(message, 'MANIFEST_NOT_FOUND', cause);
this.name = 'ManifestNotFoundError';
}
}
exports.ManifestNotFoundError = ManifestNotFoundError;
class ManifestValidationError extends ManifestError {
constructor(message, cause) {
super(message, 'MANIFEST_VALIDATION_FAILED', cause);
this.name = 'ManifestValidationError';
}
}
exports.ManifestValidationError = ManifestValidationError;
class ManifestPermissionError extends ManifestError {
constructor(message, cause) {
super(message, 'MANIFEST_PERMISSION_DENIED', cause);
this.name = 'ManifestPermissionError';
}
}
exports.ManifestPermissionError = ManifestPermissionError;
/**
* ManifestManager handles local manifest storage, retrieval, and comparison operations
*/
class ManifestManager {
/**
* Store a manifest locally in the target directory
* @param targetDirectory Directory where the box is being stored
* @param manifest Box manifest to store
* @param sourceRegistry Optional source registry
* @param sourceBoxReference Optional source box reference
* @returns Promise<void>
*/
async storeLocalManifest(targetDirectory, manifest, sourceRegistry, sourceBoxReference, isUpdate = false) {
try {
// Validate manifest before storing
try {
manifestUtils_1.ManifestUtils.validateManifest(manifest);
}
catch (error) {
throw new ManifestValidationError(`Invalid manifest for ${targetDirectory}`, error instanceof Error ? error : new Error(String(error)));
}
// Ensure .qraft directory exists
try {
await manifestUtils_1.ManifestUtils.ensureQraftDirectory(targetDirectory);
}
catch (error) {
throw new ManifestPermissionError(`Cannot create .qraft directory in ${targetDirectory}`, error instanceof Error ? error : new Error(String(error)));
}
// Store manifest using utility function
try {
await manifestUtils_1.ManifestUtils.writeManifestFile(targetDirectory, manifest);
}
catch (error) {
throw new ManifestPermissionError(`Cannot write manifest file in ${targetDirectory}`, error instanceof Error ? error : new Error(String(error)));
}
// Get existing metadata if this is an update
let existingMetadata = null;
if (isUpdate) {
try {
existingMetadata = await manifestUtils_1.ManifestUtils.readMetadataFile(targetDirectory);
}
catch (error) {
// If we can't read existing metadata, treat as new
existingMetadata = null;
}
}
const now = Date.now();
// Create and store metadata
const metadata = {
lastSyncTimestamp: now,
createdTimestamp: existingMetadata?.createdTimestamp || now,
lastModifiedTimestamp: now,
checksum: this.calculateChecksum(manifest),
lastSyncedVersion: manifest.version,
syncState: 'synced',
syncCount: (existingMetadata?.syncCount || 0) + 1,
lastRemoteChecksum: this.calculateChecksum(manifest),
metadataVersion: '1.0.0'
};
// Add optional properties only if they have values
if (sourceRegistry) {
metadata.sourceRegistry = sourceRegistry;
}
if (sourceBoxReference) {
metadata.sourceBoxReference = sourceBoxReference;
}
try {
await manifestUtils_1.ManifestUtils.writeMetadataFile(targetDirectory, metadata);
}
catch (error) {
throw new ManifestPermissionError(`Cannot write metadata file in ${targetDirectory}`, error instanceof Error ? error : new Error(String(error)));
}
}
catch (error) {
if (error instanceof ManifestError) {
throw error;
}
// For unexpected errors, wrap in ManifestError
throw new ManifestError(`Failed to store local manifest in ${targetDirectory}`, 'MANIFEST_STORE_ERROR', error instanceof Error ? error : new Error(String(error)));
}
}
/**
* Retrieve local manifest from target directory
* @param targetDirectory Directory to search for local manifest
* @returns Promise<LocalManifestEntry | null> Local manifest entry or null if not found
*/
async getLocalManifest(targetDirectory) {
try {
// Check if both files exist
if (!(await manifestUtils_1.ManifestUtils.hasCompleteLocalManifest(targetDirectory))) {
return null;
}
// Read manifest using utility function with error handling
let manifest;
try {
manifest = await manifestUtils_1.ManifestUtils.readManifestFile(targetDirectory);
}
catch (error) {
throw new ManifestCorruptionError(`Failed to read manifest file in ${targetDirectory}`, error instanceof Error ? error : new Error(String(error)));
}
// Read metadata using utility function with error handling
let metadata;
try {
metadata = await manifestUtils_1.ManifestUtils.readMetadataFile(targetDirectory);
}
catch (error) {
throw new ManifestCorruptionError(`Failed to read metadata file in ${targetDirectory}`, error instanceof Error ? error : new Error(String(error)));
}
// Verify manifest integrity
const currentChecksum = this.calculateChecksum(manifest);
if (currentChecksum !== metadata.checksum) {
throw new ManifestCorruptionError(`Manifest checksum mismatch in ${targetDirectory} - file may be corrupted. Expected: ${metadata.checksum}, Got: ${currentChecksum}`);
}
return { manifest, metadata };
}
catch (error) {
if (error instanceof ManifestError) {
throw error;
}
// For unexpected errors, wrap in ManifestError
throw new ManifestError(`Unexpected error reading local manifest in ${targetDirectory}`, 'MANIFEST_READ_ERROR', error instanceof Error ? error : new Error(String(error)));
}
}
/**
* Compare two manifests and return detailed comparison result
* @param localManifest Local manifest (can be null if no local manifest exists)
* @param remoteManifest Remote manifest
* @returns ManifestComparisonResult Detailed comparison result
*/
compareManifests(localManifest, remoteManifest) {
// If no local manifest, everything is new
if (!localManifest) {
return {
isIdentical: false,
differences: this.getNewManifestDifferences(remoteManifest),
severity: 'low' // New manifest is typically low risk
};
}
const differences = [];
// Compare each field
this.compareField('name', localManifest.name, remoteManifest.name, differences);
this.compareField('description', localManifest.description, remoteManifest.description, differences);
this.compareField('author', localManifest.author, remoteManifest.author, differences);
this.compareField('version', localManifest.version, remoteManifest.version, differences);
this.compareField('defaultTarget', localManifest.defaultTarget, remoteManifest.defaultTarget, differences);
this.compareArrayField('tags', localManifest.tags, remoteManifest.tags, differences);
this.compareArrayField('exclude', localManifest.exclude, remoteManifest.exclude, differences);
this.compareArrayField('postInstall', localManifest.postInstall, remoteManifest.postInstall, differences);
const isIdentical = differences.length === 0;
const severity = this.calculateSeverity(differences);
return {
isIdentical,
differences,
severity
};
}
/**
* Check if a local manifest exists in the target directory
* @param targetDirectory Directory to check
* @returns Promise<boolean> True if local manifest exists
*/
async hasLocalManifest(targetDirectory) {
return manifestUtils_1.ManifestUtils.hasCompleteLocalManifest(targetDirectory);
}
/**
* Remove local manifest from target directory
* @param targetDirectory Directory to remove manifest from
* @returns Promise<void>
*/
async removeLocalManifest(targetDirectory) {
await manifestUtils_1.ManifestUtils.removeQraftDirectory(targetDirectory);
}
/**
* Update sync state for a local manifest
* @param targetDirectory Directory containing the manifest
* @param newState New sync state
* @param remoteChecksum Optional remote checksum for comparison
* @returns Promise<void>
*/
async updateSyncState(targetDirectory, newState, remoteChecksum) {
const localEntry = await this.getLocalManifest(targetDirectory);
if (!localEntry) {
throw new Error('No local manifest found to update sync state');
}
const updatedMetadata = {
...localEntry.metadata,
syncState: newState,
lastModifiedTimestamp: Date.now()
};
// Update remote checksum if provided
if (remoteChecksum) {
updatedMetadata.lastRemoteChecksum = remoteChecksum;
}
await manifestUtils_1.ManifestUtils.writeMetadataFile(targetDirectory, updatedMetadata);
}
/**
* Determine sync state by comparing local and remote manifests
* @param localManifest Local manifest entry
* @param remoteManifest Remote manifest
* @returns SyncState Current sync state
*/
determineSyncState(localManifest, remoteManifest) {
if (!localManifest) {
return 'unknown';
}
const localChecksum = localManifest.metadata.checksum;
const remoteChecksum = this.calculateChecksum(remoteManifest);
const lastRemoteChecksum = localManifest.metadata.lastRemoteChecksum;
// If checksums match, we're synced
if (localChecksum === remoteChecksum) {
return 'synced';
}
// If we have a last known remote checksum, we can determine direction
if (lastRemoteChecksum) {
const localChanged = localChecksum !== lastRemoteChecksum;
const remoteChanged = remoteChecksum !== lastRemoteChecksum;
if (localChanged && remoteChanged) {
return 'diverged';
}
else if (localChanged) {
return 'local_newer';
}
else if (remoteChanged) {
return 'remote_newer';
}
}
// Compare versions if available
const localVersion = localManifest.manifest.version;
const remoteVersion = remoteManifest.version;
const lastSyncedVersion = localManifest.metadata.lastSyncedVersion;
if (lastSyncedVersion) {
if (localVersion !== lastSyncedVersion && remoteVersion !== lastSyncedVersion) {
return 'diverged';
}
else if (localVersion !== lastSyncedVersion) {
return 'local_newer';
}
else if (remoteVersion !== lastSyncedVersion) {
return 'remote_newer';
}
}
// Fallback to timestamp comparison
const localTimestamp = localManifest.metadata.lastModifiedTimestamp;
const syncTimestamp = localManifest.metadata.lastSyncTimestamp;
if (localTimestamp > syncTimestamp) {
return 'local_newer';
}
return 'unknown';
}
/**
* Get sync statistics for a local manifest
* @param targetDirectory Directory containing the manifest
* @returns Promise<SyncStats | null> Sync statistics or null if no manifest
*/
async getSyncStats(targetDirectory) {
const localEntry = await this.getLocalManifest(targetDirectory);
if (!localEntry) {
return null;
}
const now = Date.now();
const daysSinceLastSync = Math.floor((now - localEntry.metadata.lastSyncTimestamp) / (1000 * 60 * 60 * 24));
const daysSinceCreated = Math.floor((now - localEntry.metadata.createdTimestamp) / (1000 * 60 * 60 * 24));
return {
syncState: localEntry.metadata.syncState,
syncCount: localEntry.metadata.syncCount,
daysSinceLastSync,
daysSinceCreated,
lastSyncTimestamp: localEntry.metadata.lastSyncTimestamp,
createdTimestamp: localEntry.metadata.createdTimestamp,
hasRemoteChecksum: !!localEntry.metadata.lastRemoteChecksum
};
}
/**
* Update metadata when manifest is modified locally
* @param targetDirectory Directory containing the manifest
* @param updatedManifest Updated manifest
* @returns Promise<void>
*/
async updateLocalManifest(targetDirectory, updatedManifest) {
// Validate the updated manifest
manifestUtils_1.ManifestUtils.validateManifest(updatedManifest);
const existingEntry = await this.getLocalManifest(targetDirectory);
if (!existingEntry) {
throw new Error('No existing local manifest found to update');
}
// Write the updated manifest
await manifestUtils_1.ManifestUtils.writeManifestFile(targetDirectory, updatedManifest);
// Update metadata to reflect local changes
const updatedMetadata = {
...existingEntry.metadata,
lastModifiedTimestamp: Date.now(),
checksum: this.calculateChecksum(updatedManifest),
syncState: 'local_newer' // Mark as locally modified
};
await manifestUtils_1.ManifestUtils.writeMetadataFile(targetDirectory, updatedMetadata);
}
/**
* Check if local manifest needs sync based on metadata
* @param targetDirectory Directory containing the manifest
* @param maxDaysWithoutSync Maximum days without sync before flagging
* @returns Promise<boolean> True if sync is needed
*/
async needsSync(targetDirectory, maxDaysWithoutSync = 7) {
const stats = await this.getSyncStats(targetDirectory);
if (!stats) {
return false; // No local manifest, no sync needed
}
// Check if sync state indicates need for sync
if (stats.syncState === 'remote_newer' || stats.syncState === 'diverged' || stats.syncState === 'unknown') {
return true;
}
// Check if too much time has passed since last sync
if (stats.daysSinceLastSync > maxDaysWithoutSync) {
return true;
}
return false;
}
/**
* Attempt to recover from corrupted manifest files
* @param targetDirectory Directory with corrupted manifest
* @param backupDirectory Optional backup directory to restore from
* @returns Promise<ManifestRecoveryResult> Recovery result
*/
async recoverCorruptedManifest(targetDirectory, backupDirectory) {
const result = {
success: false,
method: 'none',
errors: [],
warnings: []
};
try {
// Try to restore from backup if provided
if (backupDirectory) {
try {
await manifestUtils_1.ManifestUtils.restoreQraftDirectory(targetDirectory, backupDirectory);
result.success = true;
result.method = 'backup_restore';
return result;
}
catch (error) {
result.errors.push(`Backup restore failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Try to find and restore from automatic backups
try {
const entries = await manifestUtils_1.ManifestUtils.findManifestDirectories(targetDirectory, 1);
const backupDirs = entries.filter(dir => dir.includes('.qraft-backup-'));
if (backupDirs.length > 0) {
// Use the most recent backup
const mostRecent = backupDirs.sort().pop();
await manifestUtils_1.ManifestUtils.restoreQraftDirectory(targetDirectory, mostRecent);
result.success = true;
result.method = 'auto_backup_restore';
result.warnings.push(`Restored from automatic backup: ${mostRecent}`);
return result;
}
}
catch (error) {
result.errors.push(`Auto backup search failed: ${error instanceof Error ? error.message : String(error)}`);
}
// Try to reconstruct minimal manifest from available information
try {
const reconstructed = await this.reconstructManifest(targetDirectory);
if (reconstructed) {
await this.storeLocalManifest(targetDirectory, reconstructed);
result.success = true;
result.method = 'reconstruction';
result.warnings.push('Manifest reconstructed with minimal information - please verify and update');
return result;
}
}
catch (error) {
result.errors.push(`Reconstruction failed: ${error instanceof Error ? error.message : String(error)}`);
}
// If all recovery methods fail
result.errors.push('All recovery methods failed - manual intervention required');
return result;
}
catch (error) {
result.errors.push(`Recovery process failed: ${error instanceof Error ? error.message : String(error)}`);
return result;
}
}
/**
* Attempt to reconstruct a manifest from available information
* @param targetDirectory Directory to analyze
* @returns Promise<BoxManifest | null> Reconstructed manifest or null if not possible
*/
async reconstructManifest(targetDirectory) {
try {
// Try to read partial manifest file
let partialManifest = null;
try {
const content = await manifestUtils_1.ManifestUtils.readManifestFile(targetDirectory);
partialManifest = content;
}
catch (error) {
// Manifest file is corrupted or missing
}
// Create minimal manifest with defaults only if directory name is meaningful
const directoryName = require('path').basename(targetDirectory);
// Don't reconstruct for generic or meaningless directory names
if (!directoryName ||
directoryName.length < 3 ||
/^(temp|tmp|test|dir|folder|\d+)/.test(directoryName.toLowerCase()) ||
directoryName.includes('12345')) {
return null;
}
const manifest = {
name: partialManifest?.name || directoryName,
description: partialManifest?.description || `Reconstructed manifest for ${directoryName}`,
author: partialManifest?.author || 'Unknown',
version: partialManifest?.version || '1.0.0',
defaultTarget: partialManifest?.defaultTarget,
tags: partialManifest?.tags,
exclude: partialManifest?.exclude || ['.qraft/'],
postInstall: partialManifest?.postInstall
};
// Validate the reconstructed manifest
manifestUtils_1.ManifestUtils.validateManifest(manifest);
return manifest;
}
catch (error) {
return null;
}
}
/**
* Validate manifest integrity and report issues
* @param targetDirectory Directory to validate
* @returns Promise<ManifestIntegrityResult> Integrity check result
*/
async validateManifestIntegrity(targetDirectory) {
const result = {
isValid: true,
issues: [],
warnings: [],
canRecover: false
};
try {
// Check if manifest files exist
const hasManifest = await manifestUtils_1.ManifestUtils.manifestFileExists(targetDirectory);
const hasMetadata = await manifestUtils_1.ManifestUtils.metadataFileExists(targetDirectory);
if (!hasManifest && !hasMetadata) {
result.isValid = false;
result.issues.push('No manifest files found');
result.canRecover = false;
return result;
}
if (!hasManifest) {
result.isValid = false;
result.issues.push('Manifest file missing');
result.canRecover = hasMetadata;
}
if (!hasMetadata) {
result.isValid = false;
result.issues.push('Metadata file missing');
result.canRecover = hasManifest;
}
// Try to read and validate manifest
if (hasManifest) {
try {
const manifest = await manifestUtils_1.ManifestUtils.readManifestFile(targetDirectory);
manifestUtils_1.ManifestUtils.validateManifest(manifest);
}
catch (error) {
result.isValid = false;
result.issues.push(`Manifest validation failed: ${error instanceof Error ? error.message : String(error)}`);
result.canRecover = true;
}
}
// Try to read metadata
if (hasMetadata) {
try {
await manifestUtils_1.ManifestUtils.readMetadataFile(targetDirectory);
}
catch (error) {
result.isValid = false;
result.issues.push(`Metadata read failed: ${error instanceof Error ? error.message : String(error)}`);
result.canRecover = true;
}
}
// Check checksum integrity if both files exist
if (hasManifest && hasMetadata && result.isValid) {
try {
const localEntry = await this.getLocalManifest(targetDirectory);
if (!localEntry) {
result.isValid = false;
result.issues.push('Failed to load complete manifest entry');
result.canRecover = true;
}
}
catch (error) {
if (error instanceof ManifestCorruptionError) {
result.isValid = false;
result.issues.push(error.message);
result.canRecover = true;
}
else {
result.isValid = false;
result.issues.push(`Integrity check failed: ${error instanceof Error ? error.message : String(error)}`);
result.canRecover = false;
}
}
}
return result;
}
catch (error) {
result.isValid = false;
result.issues.push(`Validation process failed: ${error instanceof Error ? error.message : String(error)}`);
result.canRecover = false;
return result;
}
}
/**
* Calculate checksum for manifest content
* @param manifest Box manifest
* @returns string SHA-256 checksum
*/
calculateChecksum(manifest) {
const content = JSON.stringify(manifest, Object.keys(manifest).sort());
return crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
}
/**
* Compare a single field between manifests
*/
compareField(field, oldValue, newValue, differences) {
if (oldValue !== newValue) {
differences.push({
field,
oldValue,
newValue,
changeType: oldValue === undefined ? 'added' : newValue === undefined ? 'removed' : 'modified',
impact: this.getFieldImpact(field)
});
}
}
/**
* Compare array fields between manifests
*/
compareArrayField(field, oldValue, newValue, differences) {
const oldArray = oldValue || [];
const newArray = newValue || [];
if (JSON.stringify(oldArray.sort()) !== JSON.stringify(newArray.sort())) {
differences.push({
field,
oldValue: oldArray,
newValue: newArray,
changeType: oldArray.length === 0 ? 'added' : newArray.length === 0 ? 'removed' : 'modified',
impact: this.getFieldImpact(field)
});
}
}
/**
* Get impact level for a specific manifest field
*/
getFieldImpact(field) {
switch (field) {
case 'version':
return 'critical'; // Version changes are always critical
case 'name':
return 'high'; // Name changes are high impact
case 'exclude':
return 'high'; // Exclude pattern changes can affect what files are copied
case 'defaultTarget':
return 'medium'; // Target changes are medium impact
case 'description':
case 'author':
case 'tags':
case 'postInstall':
return 'low'; // Metadata changes are low impact
default:
return 'medium';
}
}
/**
* Calculate overall severity from differences
*/
calculateSeverity(differences) {
if (differences.length === 0)
return 'none';
const maxImpact = differences.reduce((max, diff) => {
const impacts = ['low', 'medium', 'high', 'critical'];
const currentIndex = impacts.indexOf(diff.impact);
const maxIndex = impacts.indexOf(max);
return currentIndex > maxIndex ? diff.impact : max;
}, 'low');
return maxImpact;
}
/**
* Generate differences for a completely new manifest
*/
getNewManifestDifferences(manifest) {
const differences = [];
Object.keys(manifest).forEach(key => {
const field = key;
differences.push({
field,
oldValue: undefined,
newValue: manifest[field],
changeType: 'added',
impact: this.getFieldImpact(field)
});
});
return differences;
}
}
exports.ManifestManager = ManifestManager;
//# sourceMappingURL=manifestManager.js.map