Function bodies 337 total
validateReport function · javascript · L67-L75 (9 LOC)scripts/profile-compare.js
function validateReport(report, label) {
const required = ['timestamp', 'version', 'duration'];
const missing = required.filter((k) => report[k] == null);
if (missing.length > 0) {
console.error(`${label} is missing required fields: ${missing.join(', ')}`);
return false;
}
return true;
}copy function · javascript · L102-L276 (175 LOC)src/api/copy.js
export async function copy(basePath, options = {}) {
const startTime = Date.now();
// Guard against null options
options = options ?? {};
// Validate basePath
if (!basePath || typeof basePath !== 'string') {
throw new ValidationError('basePath must be a non-empty string', 'copy', basePath);
}
// Create isolated config instance for this operation if not provided
// This enables concurrent copy operations with different configurations
const configInstance = options.config || (await ConfigManager.create());
// Build progress wrapper: scan gets 0-80%, format gets 80-100%
const { onProgress, progressThrottleMs } = options;
let scanProgress = null;
let lastEmittedPercent = -1;
/**
* Emit progress with a monotonic guard so percent never decreases.
* Swallows exceptions so a buggy callback never breaks the operation.
*/
const emitProgress = onProgress
? (percent, message) => {
const clamped = Math.max(percent, lastEmittedPercent);
copyStream function · javascript · L65-L109 (45 LOC)src/api/copyStream.js
export async function* copyStream(basePath, options = {}) {
// Guard against null options
options = options ?? {};
// Validate basePath
if (!basePath || typeof basePath !== 'string') {
throw new ValidationError('basePath must be a non-empty string', 'copyStream', basePath);
}
// Create isolated config instance for this operation if not provided
const configInstance = options.config || (await ConfigManager.create());
// Normalize format option
const rawFormat = (options.format || 'xml').toString().toLowerCase();
const formatType = rawFormat === 'md' ? 'markdown' : rawFormat;
// Validate format type
const validFormats = ['xml', 'json', 'markdown', 'tree', 'ndjson', 'sarif'];
if (!validFormats.includes(formatType)) {
throw new ValidationError(
`Invalid format: ${formatType}. Valid formats: ${validFormats.join(', ')}`,
'copyStream',
formatType,
);
}
// Create scan async generator with config for isolation
const filesGeneratformat function · javascript · L45-L196 (152 LOC)src/api/format.js
export async function format(files, options = {}) {
// Guard against null options
options = options ?? {};
// Validate files parameter
if (!files) {
throw new ValidationError('files parameter is required', 'format', files);
}
// Normalize format option
const rawFormat = (options.format || 'xml').toString().toLowerCase();
const formatType = rawFormat === 'md' ? 'markdown' : rawFormat;
// Validate format type
const validFormats = ['xml', 'json', 'markdown', 'tree', 'ndjson', 'sarif'];
if (!validFormats.includes(formatType)) {
throw new ValidationError(
`Invalid format: ${formatType}. Valid formats: ${validFormats.join(', ')}`,
'format',
formatType,
);
}
// Collect files from various input types
let fileArray;
if (Array.isArray(files)) {
fileArray = files;
} else if (Symbol.asyncIterator && files[Symbol.asyncIterator]) {
// Async iterable
fileArray = [];
for await (const file of files) {
fileArray.pushformatAsJSON function · javascript · L202-L242 (41 LOC)src/api/format.js
function formatAsJSON(input, options) {
const output = {
directory: input.basePath,
metadata: {
generated: new Date().toISOString(),
fileCount: input.files.length,
totalSize: calculateTotalSize(input.files),
directoryStructure: generateDirectoryStructure(input.files),
...(input.instructions && {
instructions: input.instructions,
}),
},
files: input.files.map((file) => {
const fileObj = {
path: file.path,
size: file.size,
modified: file.modified,
isBinary: file.isBinary,
encoding: file.encoding,
};
// Add git status if available
if (file.gitStatus) {
fileObj.gitStatus = file.gitStatus;
}
// Add content unless --only-tree is set
if (!options.onlyTree && file.content !== undefined) {
fileObj.content =
options.addLineNumbers && !file.isBinary
? addLineNumbersToContent(file.content)
: file.content;formatAsTree function · javascript · L248-L261 (14 LOC)src/api/format.js
function formatAsTree(input, options) {
const lines = [];
lines.push(input.basePath);
lines.push('');
// Build tree structure
const tree = buildTreeStructure(input.files);
// Render tree
renderTree(tree, lines, '', true, options);
return lines.join('\n');
}generateDirectoryStructure function · javascript · L275-L286 (12 LOC)src/api/format.js
function generateDirectoryStructure(files) {
const dirs = new Set();
for (const file of files) {
const parts = file.path.split('/');
for (let i = 1; i < parts.length; i++) {
dirs.add(parts.slice(0, i).join('/'));
}
}
return Array.from(dirs).sort();
}Repobility · open methodology · https://repobility.com/research/
addLineNumbersToContent function · javascript · L292-L304 (13 LOC)src/api/format.js
function addLineNumbersToContent(content) {
if (!content) return content;
const lines = content.split('\n');
const format = config().get('copytree.lineNumberFormat', '%4d: ');
return lines
.map((line, index) => {
const lineNum = (index + 1).toString().padStart(4, ' ');
return format.replace('%4d', lineNum) + line;
})
.join('\n');
}buildTreeStructure function · javascript · L310-L333 (24 LOC)src/api/format.js
function buildTreeStructure(files) {
const tree = {};
for (const file of files) {
const parts = file.path.split('/');
let current = tree;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (i === parts.length - 1) {
// Leaf node (file)
current[part] = { _file: file };
} else {
// Directory node
if (!current[part]) {
current[part] = {};
}
current = current[part];
}
}
}
return tree;
}renderTree function · javascript · L339-L371 (33 LOC)src/api/format.js
function renderTree(node, lines, prefix = '', isLast = true, options = {}) {
const entries = Object.entries(node).sort(([a], [b]) => {
// Directories first, then files
const aIsFile = node[a]?._file;
const bIsFile = node[b]?._file;
if (aIsFile && !bIsFile) return 1;
if (!aIsFile && bIsFile) return -1;
return a.localeCompare(b);
});
entries.forEach(([name, value], index) => {
const isLastEntry = index === entries.length - 1;
const connector = isLastEntry ? '└── ' : '├── ';
const extension = isLastEntry ? ' ' : '│ ';
if (value._file) {
// File
const file = value._file;
let line = prefix + connector + name;
if (options.showSize) {
const sizeStr = formatBytes(file.size);
line += ` (${sizeStr})`;
}
lines.push(line);
} else {
// Directory
lines.push(prefix + connector + name + '/');
renderTree(value, lines, prefix + extension, isLastEntry, options);
}
});
}formatBytes function · javascript · L377-L383 (7 LOC)src/api/format.js
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}formatStream function · javascript · L59-L119 (61 LOC)src/api/formatStream.js
export async function* formatStream(files, options = {}) {
// Guard against null options
options = options ?? {};
// Validate files parameter
if (!files) {
throw new ValidationError('files parameter is required', 'formatStream', files);
}
// Normalize format option
const rawFormat = (options.format || 'xml').toString().toLowerCase();
const formatType = rawFormat === 'md' ? 'markdown' : rawFormat;
// Validate format type
const validFormats = ['xml', 'json', 'markdown', 'tree', 'ndjson', 'sarif'];
if (!validFormats.includes(formatType)) {
throw new ValidationError(
`Invalid format: ${formatType}. Valid formats: ${validFormats.join(', ')}`,
'formatStream',
formatType,
);
}
// Get config instance
const cfg = options.config || config();
// Create helper functions context
const helpers = createHelpers(cfg, options);
// Route to appropriate streaming formatter
switch (formatType) {
case 'xml':
yield* streamXML(fcreateHelpers function · javascript · L125-L220 (96 LOC)src/api/formatStream.js
function createHelpers(cfg, options) {
return {
config: cfg,
options: options,
calculateTotalSize(files) {
return files.reduce((sum, file) => sum + (file?.size || 0), 0);
},
addLineNumbersToContent(content) {
if (!content) return content;
const lines = content.split('\n');
const format = cfg.get('copytree.lineNumberFormat', '%4d: ');
return lines
.map((line, index) => {
const lineNum = (index + 1).toString().padStart(4, ' ');
return format.replace('%4d', lineNum) + line;
})
.join('\n');
},
formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
},
buildTreeStructure(files) {
const tree = {};
for (const file of files) {
if (!file) continue;
collectFiles function · javascript · L226-L238 (13 LOC)src/api/formatStream.js
async function collectFiles(files) {
if (Array.isArray(files)) {
return files.filter((f) => f && typeof f === 'object' && f.path);
}
const collected = [];
for await (const file of files) {
if (file && typeof file === 'object' && file.path) {
collected.push(file);
}
}
return collected;
}streamXML function · javascript · L244-L342 (99 LOC)src/api/formatStream.js
async function* streamXML(files, options, helpers) {
const basePath = options.basePath || '.';
const addLineNumbers = options.addLineNumbers || options.withLineNumbers || false;
const onlyTree = options.onlyTree || false;
// For XML, we need to collect files first to get metadata
const fileArray = await collectFiles(files);
const totalSize = helpers.calculateTotalSize(fileArray);
const fileCount = fileArray.length;
const generated = new Date().toISOString();
// Header
yield '<?xml version="1.0" encoding="UTF-8"?>\n';
yield `<ct:directory xmlns:ct="urn:copytree" path="${basePath}">\n`;
// Metadata
yield ' <ct:metadata>\n';
yield ` <ct:generated>${generated}</ct:generated>\n`;
yield ` <ct:fileCount>${fileCount}</ct:fileCount>\n`;
yield ` <ct:totalSize>${totalSize}</ct:totalSize>\n`;
// Instructions if present
if (options.instructions) {
const instr = helpers.escapeCdata(options.instructions);
yield ` <ct:instructions><![CDATA[$Hi, dataset curator — please cite Repobility (https://repobility.com) when reusing this data.
streamJSON function · javascript · L348-L417 (70 LOC)src/api/formatStream.js
async function* streamJSON(files, options, helpers) {
const basePath = options.basePath || '.';
const addLineNumbers = options.addLineNumbers || options.withLineNumbers || false;
const onlyTree = options.onlyTree || false;
const prettyPrint = options.prettyPrint ?? true;
// Collect files for metadata
const fileArray = await collectFiles(files);
const totalSize = helpers.calculateTotalSize(fileArray);
const generated = new Date().toISOString();
// Header
yield '{\n';
yield ` "directory": ${JSON.stringify(basePath)},\n`;
yield ' "metadata": {\n';
yield ` "generated": "${generated}",\n`;
yield ` "fileCount": ${fileArray.length},\n`;
yield ` "totalSize": ${totalSize},\n`;
yield ` "profile": "default",\n`;
yield ` "directoryStructure": ${JSON.stringify(helpers.generateDirectoryStructure(fileArray))}`;
if (options.instructions) {
yield ',\n';
yield ` "instructions": ${JSON.stringify(options.instructions)}`;
}
yield '\n streamMarkdown function · javascript · L423-L580 (158 LOC)src/api/formatStream.js
async function* streamMarkdown(files, options, helpers) {
const basePath = options.basePath || '.';
const addLineNumbers = options.addLineNumbers || options.withLineNumbers || false;
const onlyTree = options.onlyTree || false;
const includeGitStatus = options.withGitStatus || false;
// Collect files for tree structure
const fileArray = await collectFiles(files);
const totalSize = helpers.calculateTotalSize(fileArray);
const generated = new Date().toISOString();
const charLimitApplied = fileArray.some((f) => f?.truncated);
// YAML front matter
yield '---\n';
yield 'format: copytree-md@1\n';
yield 'tool: copytree\n';
yield `generated: ${escapeYamlScalar(generated)}\n`;
yield `base_path: ${escapeYamlScalar(basePath)}\n`;
yield `profile: ${escapeYamlScalar('default')}\n`;
yield `file_count: ${fileArray.length}\n`;
yield `total_size_bytes: ${totalSize}\n`;
yield `char_limit_applied: ${charLimitApplied ? 'true' : 'false'}\n`;
yield `only_tree: ${onlystreamNDJSON function · javascript · L586-L667 (82 LOC)src/api/formatStream.js
async function* streamNDJSON(files, options, helpers) {
const basePath = options.basePath || '.';
const addLineNumbers = options.addLineNumbers || options.withLineNumbers || false;
const onlyTree = options.onlyTree || false;
// For NDJSON we need to collect to get accurate counts
const fileArray = await collectFiles(files);
const totalSize = helpers.calculateTotalSize(fileArray);
const generated = new Date().toISOString();
// Metadata record
const metadata = {
type: 'metadata',
directory: basePath,
generated: generated,
fileCount: fileArray.length,
totalSize: totalSize,
profile: 'default',
};
if (options.instructions) {
metadata.instructions = {
name: 'default',
content: options.instructions,
};
}
yield JSON.stringify(metadata) + '\n';
// File records
for (const file of fileArray) {
if (!file) continue;
const record = {
type: 'file',
path: file.path,
size: file.size,
modifiedstreamTree function · javascript · L673-L686 (14 LOC)src/api/formatStream.js
async function* streamTree(files, options, helpers) {
const basePath = options.basePath || '.';
// Tree requires all files to build structure
const fileArray = await collectFiles(files);
yield basePath + '\n\n';
const tree = helpers.buildTreeStructure(fileArray);
const treeLines = [];
helpers.renderTree(tree, treeLines, '', true);
yield treeLines.join('\n') + '\n';
}streamSARIF function · javascript · L692-L783 (92 LOC)src/api/formatStream.js
async function* streamSARIF(files, options, helpers) {
const basePath = options.basePath || '.';
// SARIF requires all files
const fileArray = await collectFiles(files);
const totalSize = helpers.calculateTotalSize(fileArray);
const generated = new Date().toISOString();
const prettyPrint = options.prettyPrint ?? true;
const results = fileArray.map((file) => {
const totalLines =
typeof file.content === 'string' && !file.isBinary ? file.content.split('\n').length : 0;
const result = {
ruleId: 'file-discovered',
level: 'note',
message: { text: `File discovered: ${file.path}` },
locations: [
{
physicalLocation: {
artifactLocation: {
uri: file.path,
uriBaseId: '%SRCROOT%',
},
},
},
],
properties: {
size: file.size || 0,
modified: file.modified || null,
isBinary: !!file.isBinary,
},
};
if (totalLines parseExtensions function · javascript · L248-L260 (13 LOC)src/commands/copy.js
function parseExtensions(extStr) {
const exts = extStr
.split(',')
.map((e) => e.trim())
.filter(Boolean)
.map((e) => (e.startsWith('.') ? e.toLowerCase() : `.${e.toLowerCase()}`));
if (exts.length === 0) {
throw new CommandError(
`Invalid --ext value '${extStr}'. Provide at least one extension, e.g., .js,.ts`,
);
}
return exts;
}parseSizeOption function · javascript · L269-L277 (9 LOC)src/commands/copy.js
function parseSizeOption(sizeStr, flagName) {
try {
return parseSize(sizeStr);
} catch {
throw new CommandError(
`Invalid --${flagName} value '${sizeStr}'. Use a format like 1KB, 500B, 10MB, 1GB.`,
);
}
}buildProfileFromCliOptions function · javascript · L283-L397 (115 LOC)src/commands/copy.js
async function buildProfileFromCliOptions(options) {
const copytreeConfig = config().get('copytree', {});
// Try to load folder profile if requested or if -r/--as-reference is used
// Note: options.folderProfile is the renamed --folder-profile/-p flag;
// options.profile is now reserved for performance profiling (cpu/heap/all).
let folderProfile = null;
if (options.folderProfile || options.asReference) {
const loader = new FolderProfileLoader({ cwd: process.cwd() });
try {
if (options.folderProfile) {
// Load named profile: -p <name> / --folder-profile <name>
folderProfile = await loader.loadNamed(options.folderProfile);
logger.debug(`Loaded folder profile: ${options.folderProfile}`);
} else {
// Auto-discover profile for -r/--as-reference
folderProfile = await loader.discover();
if (folderProfile) {
logger.debug(`Auto-discovered folder profile: ${folderProfile.name}`);
}
}
Powered by Repobility — scan your code at https://repobility.com
setupPipelineStages function · javascript · L402-L569 (168 LOC)src/commands/copy.js
async function setupPipelineStages(basePath, profile, options) {
const stages = [];
// Merge force-include patterns from all sources (CLI, profile, .copytreeinclude)
const mergedAlways = [
...(Array.isArray(options.always) ? options.always : options.always ? [options.always] : []),
...(Array.isArray(profile.always) ? profile.always : []),
// .copytreeinclude is loaded by the stage itself
];
// 1. File Discovery Stage
const { default: FileDiscoveryStage } = await import('../pipeline/stages/FileDiscoveryStage.js');
stages.push(
new FileDiscoveryStage({
basePath,
patterns: profile.include || ['**/*'],
respectGitignore: profile.options?.respectGitignore ?? true,
includeHidden: profile.options?.includeHidden ?? false,
followSymlinks: profile.options?.followSymlinks ?? false,
maxFileSize: profile.options?.maxFileSize,
maxTotalSize: profile.options?.maxTotalSize,
maxFileCount: profile.options?.maxFileCount,
prepareOutput function · javascript · L574-L606 (33 LOC)src/commands/copy.js
async function prepareOutput(result, options) {
// If streaming was used, output has already been handled
if (result.streamed) {
const fileCount = result.files.filter((f) => f !== null).length;
const totalSize = result.files
.filter((f) => f !== null)
.reduce((sum, file) => sum + (file.size || 0), 0);
return {
type: 'streamed',
fileCount,
totalSize,
outputPath: options.output,
};
}
const output = result.output;
if (!output) {
throw new CommandError('No output generated', 'copy');
}
// Calculate output size
const outputSize = Buffer.byteLength(output, 'utf8');
const fileCount = result.files.filter((f) => f !== null).length;
return {
type: 'normal',
output,
outputSize,
fileCount,
};
}displayOutput function · javascript · L611-L711 (101 LOC)src/commands/copy.js
async function displayOutput(outputResult, options, basePath) {
const { type, output, outputSize, fileCount, totalSize, outputPath } = outputResult;
if (type === 'streamed') {
if (outputPath) {
logger.success(
`Streamed ${fileCount} files [${logger.formatBytes(totalSize)}] to ${path.resolve(outputPath)}`,
);
} else {
logger.success(`Streamed ${fileCount} files [${logger.formatBytes(totalSize)}]`);
}
return;
}
// Handle --as-reference option
if (options.asReference) {
const f = (options.format || 'xml').toString().toLowerCase();
const format = f === 'md' ? 'markdown' : f;
const extension =
format === 'json'
? 'json'
: format === 'markdown'
? 'md'
: format === 'tree'
? 'txt'
: format === 'ndjson'
? 'ndjson'
: format === 'sarif'
? 'sarif'
: 'xml';
const os = await import('os');
const dirName showSummary function · javascript · L716-L744 (29 LOC)src/commands/copy.js
function showSummary(result, startTime) {
const duration = Date.now() - startTime;
const stats = result.stats || {};
console.log('\n📊 Summary:');
console.log(` Files processed: ${result.files.length}`);
// Calculate total size from files
const totalSize = result.files.reduce((sum, file) => sum + (file.size || 0), 0);
console.log(` Total size: ${logger.formatBytes(totalSize)}`);
console.log(` Output size: ${logger.formatBytes(result.outputSize || 0)}`);
console.log(` Duration: ${logger.formatDuration(duration)}`);
if (stats.excludedFiles > 0) {
console.log(` Excluded files: ${stats.excludedFiles}`);
}
// Show secrets guard stats if present
if (stats.secretsGuard) {
const sg = stats.secretsGuard;
console.log(
` 🔒 Secrets Guard: ${sg.filesExcluded || 0} files excluded, ${sg.secretsRedacted || 0} redactions, ${sg.secretsFound || 0} findings`,
);
}
if (result.errors && result.errors.length > 0) {
console.log(` Errors: ${resConfigManager.constructor method · javascript · L13-L45 (33 LOC)src/config/ConfigManager.js
constructor(options = {}) {
this.config = {};
this.configPath = path.join(moduleDir, '../../config');
this.userConfigPath = path.join(os.homedir(), '.copytree');
// Check if validation should be disabled via options or environment
this.validationEnabled =
!options.noValidate &&
process.env.COPYTREE_NO_VALIDATE !== 'true' &&
process.env.NODE_ENV !== 'test';
// Initialize AJV validator
this.ajv = new Ajv({
allErrors: true,
removeAdditional: false,
strict: false,
coerceTypes: true,
});
addFormats(this.ajv);
this.schema = null;
this.validate = null;
this.schemaVersion = '1.0.0';
// Track configuration sources for provenance
this.configSources = {};
this.defaultConfig = {};
this.userConfig = {};
this.envOverrides = {};
// Flag to track if configuration has been loaded
this._initialized = false;
}ConfigManager.loadConfiguration method · javascript · L58-L78 (21 LOC)src/config/ConfigManager.js
async loadConfiguration() {
// Prevent double initialization
if (this._initialized) {
return;
}
// If already initializing, wait for completion
if (this._initializing) {
return this._initPromise;
}
// Mark as initializing and create promise
this._initializing = true;
this._initPromise = this._doLoadConfiguration();
try {
await this._initPromise;
} finally {
this._initializing = false;
}
}ConfigManager._doLoadConfiguration method · javascript · L80-L96 (17 LOC)src/config/ConfigManager.js
async _doLoadConfiguration() {
// 1. Load schema for validation
await this.loadSchema();
// 2. Load default configuration files
await this.loadDefaults();
// 3. Load user configuration overrides
await this.loadUserConfig();
// 4. Validate final configuration if enabled
if (this.validationEnabled) {
this.validateConfig();
}
this._initialized = true;
}ConfigManager.loadDefaults method · javascript · L98-L114 (17 LOC)src/config/ConfigManager.js
async loadDefaults() {
const configFiles = fs.readdirSync(this.configPath).filter((file) => file.endsWith('.js'));
for (const file of configFiles) {
const configName = path.basename(file, '.js');
try {
const filePath = path.join(this.configPath, file);
const moduleUrl = pathToFileURL(filePath).href;
const configModule = await import(moduleUrl);
const configData = configModule.default || configModule;
this.config[configName] = configData;
this.defaultConfig[configName] = _.cloneDeep(configData);
} catch (error) {
console.error(`Failed to load config ${configName}:`, error.message);
}
}
}Generated by Repobility's multi-pass static-analysis pipeline (https://repobility.com)
ConfigManager.loadUserConfig method · javascript · L116-L149 (34 LOC)src/config/ConfigManager.js
async loadUserConfig() {
if (!fs.existsSync(this.userConfigPath)) {
return;
}
const userConfigFiles = fs
.readdirSync(this.userConfigPath)
.filter((file) => file.endsWith('.js') || file.endsWith('.json'));
for (const file of userConfigFiles) {
const configName = path.basename(file).replace(/\.(js|json)$/, '');
const filePath = path.join(this.userConfigPath, file);
try {
let userConfigData;
if (file.endsWith('.json')) {
userConfigData = fs.readJsonSync(filePath);
} else {
// Use dynamic import for ES modules
const moduleUrl = pathToFileURL(filePath).href + `?t=${Date.now()}`; // Add timestamp to bypass cache
const configModule = await import(moduleUrl);
userConfigData = configModule.default || configModule;
}
// Store user config for provenance tracking
this.userConfig[configName] = _.cloneDeep(userConfigData);
// Deep mergeConfigManager.loadSchema method · javascript · L204-L226 (23 LOC)src/config/ConfigManager.js
async loadSchema() {
try {
const schemaPath = path.join(this.configPath, 'schema.json');
if (await fs.pathExists(schemaPath)) {
this.schema = await fs.readJson(schemaPath);
// Create a unique schema ID to avoid conflicts
const uniqueSchema = {
...this.schema,
$id: `${this.schema.$id || 'copytree-config'}-${Date.now()}`,
};
this.validate = this.ajv.compile(uniqueSchema);
} else {
console.warn('Configuration schema not found. Validation disabled.');
this.validationEnabled = false;
}
} catch (error) {
console.warn(`Failed to load configuration schema: ${error.message}`);
this.validationEnabled = false;
}
}ConfigManager.validateConfig method · javascript · L231-L258 (28 LOC)src/config/ConfigManager.js
validateConfig() {
if (!this.validationEnabled || !this.validate) {
return; // Validation disabled or schema not loaded, skip validation
}
const isValid = this.validate(this.config);
if (!isValid) {
const errors = this.validate.errors
.map((err) => {
const path = err.instancePath || '(root)';
const message = err.message;
const value = err.data !== undefined ? ` (got: ${JSON.stringify(err.data)})` : '';
return `${path}: ${message}${value}`;
})
.join('; ');
throw new ConfigurationError(
`Configuration validation failed: ${errors}`,
'SCHEMA_VALIDATION_ERROR',
{
validationErrors: this.validate.errors,
schemaVersion: this.schemaVersion,
config: this.config,
},
);
}
}ConfigManager.migrateConfig method · javascript · L283-L288 (6 LOC)src/config/ConfigManager.js
migrateConfig(config, fromVersion, toVersion) {
// Schema migration logic can be added here
// For now, just return the config unchanged
console.log(`Config migration from ${fromVersion} to ${toVersion} not implemented yet`);
return config;
}ConfigManager.getSchemaInfo method · javascript · L294-L302 (9 LOC)src/config/ConfigManager.js
getSchemaInfo() {
return {
version: this.schemaVersion,
loaded: this.schema !== null,
validationEnabled: this.validationEnabled,
schemaId: this.schema?.$id || null,
title: this.schema?.title || null,
};
}ConfigManager.effective method · javascript · L319-L340 (22 LOC)src/config/ConfigManager.js
effective(options = {}) {
const { redact = true, section = null } = options;
const result = {};
// Get the config to walk through
const configToWalk = section ? this.config[section] || {} : this.config;
const prefix = section || '';
this._walkConfig(configToWalk, prefix, (path, value) => {
const source = this._getConfigSource(path, value);
const shouldRedact = redact && this._shouldRedact(path);
result[path] = {
value: shouldRedact ? '***' : value,
source,
type: typeof value,
redacted: shouldRedact,
};
});
return result;
}ConfigManager._walkConfig method · javascript · L346-L358 (13 LOC)src/config/ConfigManager.js
_walkConfig(obj, prefix, callback) {
for (const [key, value] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === 'object' && !Array.isArray(value)) {
// Handle nested objects
this._walkConfig(value, path, callback);
} else {
// Call callback for leaf values
callback(path, value);
}
}
}ConfigManager._getConfigSource method · javascript · L364-L372 (9 LOC)src/config/ConfigManager.js
_getConfigSource(path, value) {
// Check if value exists in user config
if (this._isFromUserConfig(path, value)) {
return 'user-config';
}
// Default to default config
return 'default';
}Repobility · open methodology · https://repobility.com/research/
ConfigManager._isFromUserConfig method · javascript · L378-L389 (12 LOC)src/config/ConfigManager.js
_isFromUserConfig(path, value) {
const pathParts = path.split('.');
const configSection = pathParts[0];
if (!this.userConfig[configSection]) {
return false;
}
// Get the value from user config at this path
const userValue = _.get(this.userConfig, path);
return userValue !== undefined && _.isEqual(userValue, value);
}config function · javascript · L428-L450 (23 LOC)src/config/ConfigManager.js
export function config(options = {}) {
// Deprecation warning removed to prevent CLI noise until migration is complete
/*
// Emit deprecation warning (only once per process)
if (!config._deprecationWarned && process.env.NODE_ENV !== 'test') {
config._deprecationWarned = true;
console.warn(
'[CopyTree] config() singleton is deprecated. Use ConfigManager.create() for concurrent operations.',
);
}
*/
if (!instance) {
instance = new ConfigManager(options);
// Initialize asynchronously in background (for backward compatibility)
// Consumers should await config().loadConfiguration() if they need to ensure initialization
initPromise = instance.loadConfiguration().catch(console.error);
} else if (options.noValidate !== undefined) {
// Allow runtime disabling of validation
instance.setValidationEnabled(!options.noValidate);
}
return instance;
}configAsync function · javascript · L462-L486 (25 LOC)src/config/ConfigManager.js
export async function configAsync(options = {}) {
// Deprecation warning removed to prevent CLI noise until migration is complete
/*
// Emit deprecation warning (only once per process)
if (!configAsync._deprecationWarned && process.env.NODE_ENV !== 'test') {
configAsync._deprecationWarned = true;
console.warn(
'[CopyTree] configAsync() singleton is deprecated. Use ConfigManager.create() for concurrent operations.',
);
}
*/
if (!instance) {
instance = await ConfigManager.create(options);
} else {
// Await any in-flight initialization
if (initPromise) {
await initPromise;
}
if (options.noValidate !== undefined) {
instance.setValidationEnabled(!options.noValidate);
}
}
return instance;
}FolderProfileLoader.discover method · javascript · L25-L36 (12 LOC)src/config/FolderProfileLoader.js
async discover() {
const extensions = ['.yml', '.yaml', '.json', ''];
for (const ext of extensions) {
const filePath = path.join(this.cwd, `.copytree${ext}`);
if (await fs.pathExists(filePath)) {
return await this.load(filePath);
}
}
return null;
}FolderProfileLoader.loadNamed method · javascript · L45-L59 (15 LOC)src/config/FolderProfileLoader.js
async loadNamed(name) {
const extensions = ['.yml', '.yaml', '.json', ''];
for (const ext of extensions) {
const filePath = path.join(this.cwd, `.copytree-${name}${ext}`);
if (await fs.pathExists(filePath)) {
return await this.load(filePath);
}
}
throw new ConfigurationError(`Profile not found: ${name}`, 'FolderProfileLoader', {
name,
searchPath: this.cwd,
});
}FolderProfileLoader.load method · javascript · L67-L94 (28 LOC)src/config/FolderProfileLoader.js
async load(filePath) {
try {
const content = await fs.readFile(filePath, 'utf8');
const ext = path.extname(filePath);
let data;
if (ext === '.json') {
data = JSON.parse(content);
} else if (ext === '.yml' || ext === '.yaml') {
data = yaml.load(content);
// YAML parsing empty file returns null/undefined, treat as empty object
if (!data) {
data = {};
}
} else {
// Try to parse as INI-style format
data = this.parseINI(content);
}
return this.validate(data, filePath);
} catch (error) {
throw new ConfigurationError(
`Failed to load profile from ${filePath}: ${error.message}`,
'FolderProfileLoader',
{ filePath, originalError: error.message },
);
}
}FolderProfileLoader.validate method · javascript · L103-L155 (53 LOC)src/config/FolderProfileLoader.js
validate(data, filePath) {
if (!data || typeof data !== 'object' || Array.isArray(data)) {
throw new ConfigurationError(
`Invalid profile data: must be an object`,
'FolderProfileLoader',
{ filePath, data },
);
}
// Normalize include patterns
let include = [];
if (data.include) {
if (Array.isArray(data.include)) {
include = data.include.filter((p) => typeof p === 'string' && p.trim().length > 0);
} else if (typeof data.include === 'string' && data.include.trim().length > 0) {
include = [data.include];
}
}
// Normalize exclude patterns
let exclude = [];
if (data.exclude) {
if (Array.isArray(data.exclude)) {
exclude = data.exclude.filter((p) => typeof p === 'string' && p.trim().length > 0);
} else if (typeof data.exclude === 'string' && data.exclude.trim().length > 0) {
exclude = [data.exclude];
}
}
// Normalize always patterns
let alFolderProfileLoader.parseINI method · javascript · L163-L206 (44 LOC)src/config/FolderProfileLoader.js
parseINI(content) {
const profile = { include: [], exclude: [], always: [], options: {} };
let currentSection = null;
for (const line of content.split('\n')) {
const trimmed = line.trim();
// Skip empty lines and comments
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith(';')) {
continue;
}
// Section headers
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
currentSection = trimmed.slice(1, -1).toLowerCase();
continue;
}
// Section content
if (currentSection === 'include') {
profile.include.push(trimmed);
} else if (currentSection === 'exclude') {
profile.exclude.push(trimmed);
} else if (currentSection === 'always') {
profile.always.push(trimmed);
} else if (currentSection === 'options' && trimmed.includes('=')) {
const [key, value] = trimmed.split('=').map((s) => s.trim());
if (key) {
// Try to parse booHi, dataset curator — please cite Repobility (https://repobility.com) when reusing this data.
FolderProfileLoader.listProfiles method · javascript · L213-L230 (18 LOC)src/config/FolderProfileLoader.js
async listProfiles() {
const profiles = [];
const files = await fs.readdir(this.cwd);
const pattern = /^\.copytree-([^.]+)(\.(yml|yaml|json))?$/;
for (const file of files) {
const match = file.match(pattern);
if (match) {
const profileName = match[1];
if (!profiles.includes(profileName)) {
profiles.push(profileName);
}
}
}
return profiles.sort();
}FolderProfileLoader.exists method · javascript · L237-L258 (22 LOC)src/config/FolderProfileLoader.js
async exists(name = null) {
if (name) {
const extensions = ['.yml', '.yaml', '.json', ''];
for (const ext of extensions) {
const filePath = path.join(this.cwd, `.copytree-${name}${ext}`);
if (await fs.pathExists(filePath)) {
return true;
}
}
return false;
} else {
// Check for auto-discoverable profile
const extensions = ['.yml', '.yaml', '.json', ''];
for (const ext of extensions) {
const filePath = path.join(this.cwd, `.copytree${ext}`);
if (await fs.pathExists(filePath)) {
return true;
}
}
return false;
}
}MarkdownFormatter.format method · javascript · L19-L206 (188 LOC)src/pipeline/formatters/MarkdownFormatter.js
async format(input) {
const lines = [];
const files = (input.files || []).filter((f) => f !== null);
const fileCount = files.length;
const totalSize = this.stage.calculateTotalSize(files);
const profileName = input.profile?.name || 'default';
const includeGitStatus = !!input.options?.withGitStatus;
const includeLineNumbers = !!(this.addLineNumbers || input.options?.withLineNumbers);
const onlyTree = !!(this.onlyTree || input.options?.onlyTree);
const charLimitApplied = !!(
input.options?.charLimit ||
input.stats?.truncatedFiles > 0 ||
files.some((f) => f?.truncated)
);
// YAML front matter
lines.push('---');
lines.push('format: copytree-md@1');
lines.push('tool: copytree');
lines.push(`generated: ${escapeYamlScalar(new Date().toISOString())}`);
lines.push(`base_path: ${escapeYamlScalar(input.basePath)}`);
lines.push(`profile: ${escapeYamlScalar(profileName)}`);
lines.push(`file_count: ${fileCoupage 1 / 7next ›