sead-method-core
Version:
Specification Enforced Agentic Agile Development - A hybrid methodology preventing AI agent drift through catalog-based constraints with comprehensive external asset integration
390 lines (321 loc) • 11.3 kB
JavaScript
const fs = require('fs-extra');
const path = require('node:path');
const crypto = require('node:crypto');
const yaml = require('js-yaml');
const chalk = require('chalk');
const { createReadStream, createWriteStream, promises: fsPromises } = require('node:fs');
const { pipeline } = require('node:stream/promises');
const resourceLocator = require('./resource-locator');
class FileManager {
constructor() {}
async copyFile(source, destination) {
try {
await fs.ensureDir(path.dirname(destination));
// Use streaming for large files (> 10MB)
const stats = await fs.stat(source);
await (stats.size > 10 * 1024 * 1024
? pipeline(createReadStream(source), createWriteStream(destination))
: fs.copy(source, destination));
return true;
} catch (error) {
console.error(chalk.red(`Failed to copy ${source}:`), error.message);
return false;
}
}
async copyDirectory(source, destination) {
try {
await fs.ensureDir(destination);
// Use streaming copy for large directories
const files = await resourceLocator.findFiles('**/*', {
cwd: source,
nodir: true,
});
// Process files in batches to avoid memory issues
const batchSize = 50;
for (let index = 0; index < files.length; index += batchSize) {
const batch = files.slice(index, index + batchSize);
await Promise.all(
batch.map((file) => this.copyFile(path.join(source, file), path.join(destination, file))),
);
}
return true;
} catch (error) {
console.error(chalk.red(`Failed to copy directory ${source}:`), error.message);
return false;
}
}
async copyGlobPattern(pattern, sourceDir, destDir, rootValue = null) {
const files = await resourceLocator.findFiles(pattern, { cwd: sourceDir });
const copied = [];
for (const file of files) {
const sourcePath = path.join(sourceDir, file);
const destinationPath = path.join(destDir, file);
// Use root replacement if rootValue is provided and file needs it
const needsRootReplacement =
rootValue && (file.endsWith('.md') || file.endsWith('.yaml') || file.endsWith('.yml'));
let success = false;
success = await (needsRootReplacement
? this.copyFileWithRootReplacement(sourcePath, destinationPath, rootValue)
: this.copyFile(sourcePath, destinationPath));
if (success) {
copied.push(file);
}
}
return copied;
}
async calculateFileHash(filePath) {
try {
// Use streaming for hash calculation to reduce memory usage
const stream = createReadStream(filePath);
const hash = crypto.createHash('sha256');
for await (const chunk of stream) {
hash.update(chunk);
}
return hash.digest('hex').slice(0, 16);
} catch {
return null;
}
}
async createManifest(installDir, config, files) {
const manifestPath = path.join(installDir, this.manifestDir, this.manifestFile);
// Read version from package.json
let coreVersion = 'unknown';
try {
const packagePath = path.join(__dirname, '..', '..', '..', 'package.json');
const packageJson = require(packagePath);
coreVersion = packageJson.version;
} catch {
console.warn("Could not read version from package.json, using 'unknown'");
}
const manifest = {
version: coreVersion,
installed_at: new Date().toISOString(),
install_type: config.installType,
agent: config.agent || null,
ides_setup: config.ides || [],
expansion_packs: config.expansionPacks || [],
files: [],
};
// Add file information
for (const file of files) {
const filePath = path.join(installDir, file);
const hash = await this.calculateFileHash(filePath);
manifest.files.push({
path: file,
hash: hash,
modified: false,
});
}
// Write manifest
await fs.ensureDir(path.dirname(manifestPath));
await fs.writeFile(manifestPath, yaml.dump(manifest, { indent: 2 }));
return manifest;
}
async readManifest(installDir) {
const manifestPath = path.join(installDir, this.manifestDir, this.manifestFile);
try {
const content = await fs.readFile(manifestPath, 'utf8');
return yaml.load(content);
} catch {
return null;
}
}
async readExpansionPackManifest(installDir, packId) {
const manifestPath = path.join(installDir, `.${packId}`, this.manifestFile);
try {
const content = await fs.readFile(manifestPath, 'utf8');
return yaml.load(content);
} catch {
return null;
}
}
async checkModifiedFiles(installDir, manifest) {
const modified = [];
for (const file of manifest.files) {
const filePath = path.join(installDir, file.path);
const currentHash = await this.calculateFileHash(filePath);
if (currentHash && currentHash !== file.hash) {
modified.push(file.path);
}
}
return modified;
}
async checkFileIntegrity(installDir, manifest) {
const result = {
missing: [],
modified: [],
};
for (const file of manifest.files) {
const filePath = path.join(installDir, file.path);
// Skip checking the manifest file itself - it will always be different due to timestamps
if (file.path.endsWith('install-manifest.yaml')) {
continue;
}
if (await this.pathExists(filePath)) {
const currentHash = await this.calculateFileHash(filePath);
if (currentHash && currentHash !== file.hash) {
result.modified.push(file.path);
}
} else {
result.missing.push(file.path);
}
}
return result;
}
async backupFile(filePath) {
const backupPath = filePath + '.bak';
let counter = 1;
let finalBackupPath = backupPath;
// Find a unique backup filename
while (await fs.pathExists(finalBackupPath)) {
finalBackupPath = `${filePath}.bak${counter}`;
counter++;
}
await fs.copy(filePath, finalBackupPath);
return finalBackupPath;
}
async ensureDirectory(dirPath) {
try {
await fs.ensureDir(dirPath);
return true;
} catch (error) {
throw error;
}
}
async pathExists(filePath) {
return fs.pathExists(filePath);
}
async readFile(filePath) {
return fs.readFile(filePath, 'utf8');
}
async writeFile(filePath, content) {
await fs.ensureDir(path.dirname(filePath));
await fs.writeFile(filePath, content);
}
async removeDirectory(dirPath) {
await fs.remove(dirPath);
}
async createExpansionPackManifest(installDir, packId, config, files) {
const manifestPath = path.join(installDir, `.${packId}`, this.manifestFile);
const manifest = {
version: config.expansionPackVersion || require('../../../package.json').version,
installed_at: new Date().toISOString(),
install_type: config.installType,
expansion_pack_id: config.expansionPackId,
expansion_pack_name: config.expansionPackName,
ides_setup: config.ides || [],
files: [],
};
// Add file information
for (const file of files) {
const filePath = path.join(installDir, file);
const hash = await this.calculateFileHash(filePath);
manifest.files.push({
path: file,
hash: hash,
modified: false,
});
}
// Write manifest
await fs.ensureDir(path.dirname(manifestPath));
await fs.writeFile(manifestPath, yaml.dump(manifest, { indent: 2 }));
return manifest;
}
async modifyCoreConfig(installDir, config) {
const coreConfigPath = path.join(installDir, '.sead-core', 'core-config.yaml');
try {
// Read the existing core-config.yaml
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
const coreConfig = yaml.load(coreConfigContent);
// Modify sharding settings if provided
if (config.prdSharded !== undefined) {
coreConfig.prd.prdSharded = config.prdSharded;
}
if (config.architectureSharded !== undefined) {
coreConfig.architecture.architectureSharded = config.architectureSharded;
}
// Write back the modified config
await fs.writeFile(coreConfigPath, yaml.dump(coreConfig, { indent: 2 }));
return true;
} catch (error) {
console.error(chalk.red(`Failed to modify core-config.yaml:`), error.message);
return false;
}
}
async copyFileWithRootReplacement(source, destination, rootValue) {
try {
// Check file size to determine if we should stream
const stats = await fs.stat(source);
if (stats.size > 5 * 1024 * 1024) {
// 5MB threshold
// Use streaming for large files
const { Transform } = require('node:stream');
const replaceStream = new Transform({
transform(chunk, encoding, callback) {
const modified = chunk.toString().replaceAll('{root}', rootValue);
callback(null, modified);
},
});
await this.ensureDirectory(path.dirname(destination));
await pipeline(
createReadStream(source, { encoding: 'utf8' }),
replaceStream,
createWriteStream(destination, { encoding: 'utf8' }),
);
} else {
// Regular approach for smaller files
const content = await fsPromises.readFile(source, 'utf8');
const updatedContent = content.replaceAll('{root}', rootValue);
await this.ensureDirectory(path.dirname(destination));
await fsPromises.writeFile(destination, updatedContent, 'utf8');
}
return true;
} catch (error) {
console.error(chalk.red(`Failed to copy ${source} with root replacement:`), error.message);
return false;
}
}
async copyDirectoryWithRootReplacement(
source,
destination,
rootValue,
fileExtensions = ['.md', '.yaml', '.yml'],
) {
try {
await this.ensureDirectory(destination);
// Get all files in source directory
const files = await resourceLocator.findFiles('**/*', {
cwd: source,
nodir: true,
});
let replacedCount = 0;
for (const file of files) {
const sourcePath = path.join(source, file);
const destinationPath = path.join(destination, file);
// Check if this file type should have {root} replacement
const shouldReplace = fileExtensions.some((extension) => file.endsWith(extension));
if (shouldReplace) {
if (await this.copyFileWithRootReplacement(sourcePath, destinationPath, rootValue)) {
replacedCount++;
}
} else {
// Regular copy for files that don't need replacement
await this.copyFile(sourcePath, destinationPath);
}
}
if (replacedCount > 0) {
console.log(chalk.dim(` Processed ${replacedCount} files with {root} replacement`));
}
return true;
} catch (error) {
console.error(
chalk.red(`Failed to copy directory ${source} with root replacement:`),
error.message,
);
return false;
}
}
manifestDir = '.sead-core';
manifestFile = 'install-manifest.yaml';
}
module.exports = new FileManager();