@stillrivercode/agentic-workflow-template
Version:
NPM package to create AI-powered GitHub workflow automation projects
274 lines (243 loc) • 8.08 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
/**
* Validate and sanitize file path to prevent path traversal attacks
* @param {string} filePath - The file path to validate
* @returns {string} Sanitized file path
* @throws {Error} If path contains traversal attempts
*/
function sanitizePath(filePath) {
// Remove any leading/trailing whitespace and null bytes
const cleanPath = filePath.replace(/[\0\r\n]/g, '').trim();
// Check for path traversal attempts
if (
cleanPath.includes('..') ||
path.isAbsolute(cleanPath) ||
cleanPath.includes('\0') ||
cleanPath.startsWith('/') ||
cleanPath.startsWith('\\')
) {
throw new Error(`Invalid file path: ${filePath}`);
}
// Validate path components don't contain dangerous characters
const pathComponents = cleanPath.split(path.sep);
for (const component of pathComponents) {
if (component === '.' || component === '..') {
throw new Error(`Invalid path component: ${component} in ${filePath}`);
}
// Skip empty components (can happen with trailing slashes)
if (component === '') {
continue;
}
}
// Return the normalized path (safe since we've validated no traversal)
return path.normalize(cleanPath);
}
// Load file list directly from package.json to ensure consistency
const packageJson = require('../package.json');
/**
* Get files to distribute based on template type
* Uses package.json files array as the single source of truth
* @param {string} templateType - The template type (default, minimal, enterprise)
* @returns {string[]} Array of file paths to copy
*/
function getFilesToDistribute(templateType = 'default') {
let filesToCopy = [...packageJson.files];
// Filter out exclusion patterns (files starting with !)
filesToCopy = filesToCopy.filter((file) => !file.startsWith('!'));
// For minimal template, exclude docs
if (templateType === 'minimal') {
filesToCopy = filesToCopy.filter((file) => !file.includes('docs/'));
}
// For enterprise template, add enterprise-specific files if they exist
if (templateType === 'enterprise') {
const enterpriseFiles = ['monitoring/', 'advanced-scripts/'];
filesToCopy.push(...enterpriseFiles);
}
return filesToCopy;
}
/**
* Get package.json files list (used for npm packaging)
* @returns {string[]} Array of files to include in npm package
*/
function getPackageFiles() {
return [...packageJson.files];
}
/**
* Get exclusion filters for file copying
* @returns {string[]} Array of patterns to exclude
*/
function getExclusionFilters() {
return [
'dev-docs/',
'tests/',
'venv/',
'.pytest_cache/',
'node_modules/',
'.git/',
];
}
/**
* Validate that required files exist in the template
* @param {string} templateDir - Path to template directory
* @returns {Promise<{valid: boolean, missing: string[], warnings: string[]}>}
*/
async function validateTemplateFiles(templateDir) {
const result = {
valid: true,
missing: [],
warnings: [],
};
// Check required files from package.json
const requiredFiles = [
'.pre-commit-config.yaml',
'package.json',
'README.md',
'LICENSE',
];
for (const file of requiredFiles) {
try {
const sanitizedFile = sanitizePath(file);
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
const filePath = path.join(templateDir, sanitizedFile);
if (!(await fs.pathExists(filePath))) {
result.valid = false;
result.missing.push(file);
}
} catch (error) {
result.valid = false;
result.missing.push(file);
console.error(chalk.red(`Invalid file path: ${file} - ${error.message}`));
}
}
// Check optional files (warnings only)
const optionalFiles = ['.yamllint.yaml', '.secrets.baseline'];
for (const file of optionalFiles) {
try {
const sanitizedFile = sanitizePath(file);
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
const filePath = path.join(templateDir, sanitizedFile);
if (!(await fs.pathExists(filePath))) {
result.warnings.push(file);
}
} catch (error) {
result.warnings.push(file);
console.error(
chalk.yellow(`Invalid optional file path: ${file} - ${error.message}`)
);
}
}
return result;
}
/**
* Copy files with validation and proper error handling
* @param {string} templateDir - Source template directory
* @param {string} targetDir - Target project directory
* @param {string} templateType - Template type
* @returns {Promise<{success: boolean, copied: string[], skipped: string[], errors: string[]}>}
*/
async function copyTemplateFiles(
templateDir,
targetDir,
templateType = 'default'
) {
const result = {
success: true,
copied: [],
skipped: [],
errors: [],
};
// Validate template files first
const validation = await validateTemplateFiles(templateDir);
if (!validation.valid) {
result.success = false;
result.errors.push(
`Missing required files: ${validation.missing.join(', ')}`
);
return result;
}
// Log warnings for missing optional files
if (validation.warnings.length > 0) {
console.log(
chalk.yellow(
` ⚠️ Optional files not found: ${validation.warnings.join(', ')}`
)
);
}
const filesToCopy = getFilesToDistribute(templateType);
const exclusionFilters = getExclusionFilters();
for (const file of filesToCopy) {
try {
const sanitizedFile = sanitizePath(file);
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
const srcPath = path.join(templateDir, sanitizedFile);
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
const destPath = path.join(targetDir, sanitizedFile);
if (await fs.pathExists(srcPath)) {
await fs.copy(srcPath, destPath, {
filter: (src) => {
// Apply exclusion filters
return !exclusionFilters.some((filter) => src.includes(filter));
},
});
result.copied.push(file);
} else {
result.skipped.push(file);
console.log(chalk.yellow(` ⚠️ Template file not found: ${file}`));
}
} catch (error) {
result.success = false;
if (error.message.includes('Invalid file path')) {
result.errors.push(`Security error - invalid file path: ${file}`);
} else {
result.errors.push(`Failed to copy ${file}: ${error.message}`);
}
}
}
return result;
}
/**
* Update package.json files list to match current configuration
* @param {string} packageJsonPath - Path to package.json file
* @returns {Promise<boolean>} Success status
*/
async function updatePackageJsonFiles(packageJsonPath) {
try {
const packageJson = await fs.readJson(packageJsonPath);
packageJson.files = getPackageFiles();
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
return true;
} catch (error) {
console.error(
chalk.red(`Failed to update package.json files: ${error.message}`)
);
return false;
}
}
/**
* Get file distribution statistics
* @param {string} templateType - Template type
* @returns {object} Statistics about file distribution
*/
function getDistributionStats(templateType = 'default') {
const filesToCopy = getFilesToDistribute(templateType);
const baseFiles = packageJson.files.length;
const stats = {
totalFiles: filesToCopy.length,
baseFiles: baseFiles,
templateType,
sourceFiles: packageJson.files, // Show which files come from package.json
};
return stats;
}
module.exports = {
getFilesToDistribute,
getPackageFiles,
getExclusionFilters,
validateTemplateFiles,
copyTemplateFiles,
updatePackageJsonFiles,
getDistributionStats,
sanitizePath,
};