← back to gregpriday__copytree

Function bodies 337 total

All specs Real LLM only Function bodies
NDJSONFormatter.stream method · javascript · L37-L42 (6 LOC)
src/pipeline/formatters/NDJSONFormatter.js
  async stream(input, writer) {
    if (!writer || typeof writer !== 'function') {
      throw new Error('NDJSONFormatter.stream requires a writer function');
    }
    await this._produce(input, (line) => writer(line + '\n'));
  }
NDJSONFormatter._produce method · javascript · L51-L155 (105 LOC)
src/pipeline/formatters/NDJSONFormatter.js
  async _produce(input, write) {
    const files = (input.files || []).filter((f) => f !== null);
    const totalSize = this.stage.calculateTotalSize(files);
    const profileName = input.profile?.name || 'default';

    // Metadata record
    const metadata = {
      type: 'metadata',
      directory: input.basePath,
      generated: new Date().toISOString(),
      fileCount: files.length,
      totalSize,
      profile: profileName,
    };

    // Add git metadata if present
    if (input.gitMetadata) {
      metadata.git = {
        branch: input.gitMetadata.branch || null,
        lastCommit: input.gitMetadata.lastCommit
          ? {
              hash: input.gitMetadata.lastCommit.hash,
              message: input.gitMetadata.lastCommit.message,
            }
          : null,
        filterType: input.gitMetadata.filterType || null,
        hasUncommittedChanges: input.gitMetadata.hasUncommittedChanges || false,
      };
    }

    // Add instructions if present
    if (input.i
SARIFFormatter.format method · javascript · L27-L101 (75 LOC)
src/pipeline/formatters/SARIFFormatter.js
  async format(input) {
    const toolName = 'CopyTree';
    const toolVersion = input.version || '0.0.0';
    const informationUri = 'https://copytree.dev';

    const files = (input.files || []).filter((f) => f !== null);
    const results = files.map((file) => this._fileToResult(file, input));

    const sarif = {
      $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
      version: '2.1.0',
      runs: [
        {
          tool: {
            driver: {
              name: toolName,
              version: toolVersion,
              informationUri,
              rules: [
                {
                  id: 'file-discovered',
                  name: 'FileDiscovered',
                  shortDescription: {
                    text: 'A file was discovered by CopyTree.',
                  },
                  fullDescription: {
                    text: 'CopyTree enumerated this file in the selected scope based on the configured profile and filters.',
                  },
  
SARIFFormatter._fileToResult method · javascript · L110-L169 (60 LOC)
src/pipeline/formatters/SARIFFormatter.js
  _fileToResult(file, input) {
    // Calculate line count from content
    let totalLines = 0;
    if (typeof file.content === 'string' && !this.onlyTree && !file.isBinary) {
      totalLines = file.content.split('\n').length;
    }

    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,
      },
    };

    // Add region if we have content with line counts
    if (totalLines > 0) {
      result.locations[0].physicalLocation.region = {
        startLine: 1,
        endLine: Math.max(1, totalLines),
      };
    }

    // Add optional properties
    if (file.encoding)
SARIFFormatter._toFileUri method · javascript · L177-L192 (16 LOC)
src/pipeline/formatters/SARIFFormatter.js
  _toFileUri(fileSystemPath) {
    if (!fileSystemPath) {
      return undefined;
    }

    if (fileSystemPath.startsWith('file://')) {
      return fileSystemPath;
    }

    if (path.isAbsolute(fileSystemPath)) {
      return pathToFileURL(fileSystemPath).href;
    }

    // Normalize relative Windows-style paths for SARIF consumers
    return fileSystemPath.replace(/\\/g, '/');
  }
XMLFormatter.escapeCdata method · javascript · L17-L22 (6 LOC)
src/pipeline/formatters/XMLFormatter.js
  escapeCdata(content) {
    // First sanitize to remove invalid XML control characters
    const sanitized = sanitizeForXml(content.toString());
    // Then escape the ]]> sequence
    return sanitized.replaceAll(']]>', ']]]]><![CDATA[>');
  }
XMLFormatter.format method · javascript · L24-L151 (128 LOC)
src/pipeline/formatters/XMLFormatter.js
  async format(input) {
    const chunks = [];
    const generated = new Date().toISOString();
    const totalSize = this.stage.calculateTotalSize(input.files);
    const fileCount = input.files.filter((f) => f !== null).length;

    // Manual XML construction to avoid any escaping
    chunks.push('<?xml version="1.0" encoding="UTF-8"?>\n');
    chunks.push(`<ct:directory xmlns:ct="urn:copytree" path="${input.basePath}">\n`);

    // Add metadata
    chunks.push('  <ct:metadata>\n');
    chunks.push(`    <ct:generated>${generated}</ct:generated>\n`);
    chunks.push(`    <ct:fileCount>${fileCount}</ct:fileCount>\n`);
    chunks.push(`    <ct:totalSize>${totalSize}</ct:totalSize>\n`);

    if (input.profile) {
      chunks.push(`    <ct:profile>${input.profile.name || 'default'}</ct:profile>\n`);
    }

    // Add git metadata if present
    if (input.gitMetadata) {
      chunks.push('    <ct:git>\n');
      if (input.gitMetadata.branch) {
        chunks.push(`      <ct:branch>${input.g
Repobility — the code-quality scanner for AI-generated software · https://repobility.com
Pipeline.constructor method · javascript · L18-L62 (45 LOC)
src/pipeline/Pipeline.js
  constructor(options = {}) {
    super();

    this.stages = [];
    this.stageInstances = []; // Track instantiated stages for lifecycle hooks

    // Store config instance if provided, otherwise will be created during init
    this._configInstance = options.config || null;

    this.options = {
      continueOnError: options.continueOnError, // Will be lazy-loaded from config if not provided
      emitProgress: options.emitProgress, // Will be lazy-loaded from config if not provided
      parallel: options.parallel ?? false,
      maxConcurrency: options.maxConcurrency, // Will be lazy-loaded from config if not provided
      ...options,
    };

    this.stats = {
      startTime: null,
      endTime: null,
      stagesCompleted: 0,
      stagesFailed: 0,
      errors: [],
      perStageTimings: {},
      perStageMetrics: {},
      totalStageTime: 0,
      averageStageTime: 0,
    };

    // Create pipeline context for stages
    // Note: config will be populated during initializati
Pipeline.through method · javascript · L69-L76 (8 LOC)
src/pipeline/Pipeline.js
  through(stages) {
    if (!Array.isArray(stages)) {
      stages = [stages];
    }

    this.stages.push(...stages);
    return this;
  }
Pipeline._initializeStages method · javascript · L84-L127 (44 LOC)
src/pipeline/Pipeline.js
  async _initializeStages() {
    if (this.stageInstances.length > 0) {
      // Already initialized
      return;
    }

    // Create isolated config instance if not provided
    if (!this._configInstance) {
      this._configInstance = await ConfigManager.create();
    }
    // Update context with config instance
    this.context.config = this._configInstance;

    // Instantiate all stages and call onInit hooks
    for (let i = 0; i < this.stages.length; i++) {
      const Stage = this.stages[i];
      let stageInstance;

      if (typeof Stage === 'function' && !Stage.prototype) {
        // It's a plain function, use it directly
        stageInstance = Stage;
      } else if (typeof Stage === 'object' && Stage.process) {
        // It's already an instance with a process method
        stageInstance = Stage;
      } else {
        // It's a constructor, instantiate it with pipeline reference
        stageInstance = new Stage({ ...this.options, pipeline: this });
      }

      th
Pipeline.process method · javascript · L134-L196 (63 LOC)
src/pipeline/Pipeline.js
  async process(input) {
    // Initialize stages if not already done (also creates isolated config if needed)
    await this._initializeStages();

    // Refresh options from config now that we are async and likely fully loaded
    // Only override if not explicitly provided in constructor options
    // Use the isolated config instance, not the singleton
    const cfg = this._configInstance;
    if (this.options.continueOnError === undefined) {
      this.options.continueOnError = cfg.get('pipeline.continueOnError', false);
    }
    if (this.options.emitProgress === undefined) {
      this.options.emitProgress = cfg.get('pipeline.emitProgress', true);
    }
    if (this.options.maxConcurrency === undefined) {
      this.options.maxConcurrency = cfg.get('app.maxConcurrency', 5);
    }
    // Update context options as well
    this.context.options = this.options;

    this.stats.startTime = Date.now();
    this.stats.stagesCompleted = 0;
    this.stats.stagesFailed = 0;
    this.stats
Pipeline._processSequential method · javascript · L202-L376 (175 LOC)
src/pipeline/Pipeline.js
  async _processSequential(input) {
    let result = input;

    for (let i = 0; i < this.stageInstances.length; i++) {
      const stageInstance = this.stageInstances[i];
      const Stage = this.stages[i]; // For name resolution
      const stageName = this._getStageName(Stage, i);

      try {
        this.emit('stage:start', {
          stage: stageName,
          index: i,
          input: result,
        });

        const processMethod = stageInstance.process || stageInstance;

        if (typeof processMethod !== 'function') {
          throw new Error(`Stage ${stageName} does not have a process method`);
        }

        // Call beforeRun hook if it exists
        if (typeof stageInstance.beforeRun === 'function') {
          try {
            await stageInstance.beforeRun(result);
          } catch (hookError) {
            this.context.logger.warn(
              `Stage ${stageName} beforeRun hook failed: ${hookError.message}`,
            );
            // Continue process
Pipeline._processParallel method · javascript · L382-L407 (26 LOC)
src/pipeline/Pipeline.js
  async _processParallel(input) {
    const chunks = Array.isArray(input) ? input : [input];
    const results = [];

    // Process chunks in batches
    for (let i = 0; i < chunks.length; i += this.options.maxConcurrency) {
      const batch = chunks.slice(i, i + this.options.maxConcurrency);

      const batchResults = await Promise.all(
        batch.map(async (chunk, _index) => {
          try {
            return await this._processSequential(chunk);
          } catch (error) {
            if (!this.options.continueOnError) {
              throw error;
            }
            return null;
          }
        }),
      );

      results.push(...batchResults);
    }

    return Array.isArray(input) ? results : results[0];
  }
Pipeline._getStageName method · javascript · L413-L418 (6 LOC)
src/pipeline/Pipeline.js
  _getStageName(Stage, index) {
    if (typeof Stage === 'string') return Stage;
    if (Stage.name) return Stage.name;
    if (Stage.constructor && Stage.constructor.name) return Stage.constructor.name;
    return `Stage${index + 1}`;
  }
Pipeline.getStats method · javascript · L424-L438 (15 LOC)
src/pipeline/Pipeline.js
  getStats() {
    // Calculate average stage time if we have completed stages
    const totalStages = this.stats.stagesCompleted + this.stats.stagesFailed;
    if (totalStages > 0 && this.stats.totalStageTime > 0) {
      this.stats.averageStageTime = this.stats.totalStageTime / totalStages;
    }

    return {
      ...this.stats,
      duration: this.stats.endTime
        ? this.stats.endTime - this.stats.startTime
        : Date.now() - this.stats.startTime,
      successRate: totalStages > 0 ? this.stats.stagesCompleted / totalStages : 1,
    };
  }
Open data scored by Repobility · https://repobility.com
Pipeline.send method · javascript · L455-L474 (20 LOC)
src/pipeline/Pipeline.js
  send(passable) {
    const self = this;
    return {
      through(stages) {
        self.through(stages);
        return {
          async then(callback) {
            const result = await self.process(passable);
            if (callback) {
              return callback(result);
            }
            return result;
          },
          async thenReturn() {
            return await self.process(passable);
          },
        };
      },
    };
  }
Pipeline.through method · javascript · L458-L472 (15 LOC)
src/pipeline/Pipeline.js
      through(stages) {
        self.through(stages);
        return {
          async then(callback) {
            const result = await self.process(passable);
            if (callback) {
              return callback(result);
            }
            return result;
          },
          async thenReturn() {
            return await self.process(passable);
          },
        };
      },
Pipeline.then method · javascript · L461-L467 (7 LOC)
src/pipeline/Pipeline.js
          async then(callback) {
            const result = await self.process(passable);
            if (callback) {
              return callback(result);
            }
            return result;
          },
Stage.constructor method · javascript · L14-L25 (12 LOC)
src/pipeline/Stage.js
  constructor(options = {}) {
    this.options = options;
    // Config is now provided via the pipeline context in onInit()
    // For backward compatibility, accept config in options, but prefer context
    this._config = options.config || null;
    this.name = this.constructor.name;
    this.pipeline = options.pipeline; // Reference to parent pipeline for event emission

    // Performance optimization: throttle file events
    this.fileEventCount = 0;
    this.lastFileEventTime = 0;
  }
Stage.onInit method · javascript · L102-L108 (7 LOC)
src/pipeline/Stage.js
  async onInit(context) {
    // Set config from pipeline context if not already set
    if (context?.config && !this._config) {
      this._config = context.config;
    }
    // Default implementation - subclasses can override for custom initialization
  }
Stage.log method · javascript · L161-L193 (33 LOC)
src/pipeline/Stage.js
  log(message, level = 'info') {
    // Emit event for UI if pipeline is available
    if (this.pipeline) {
      this.pipeline.emit('stage:log', {
        stage: this.name,
        message,
        level,
        timestamp: Date.now(),
      });
    }

    // Route all terminal output through the central logger so that
    // --log-level, --log-format, --no-color, and COPYTREE_LOG_LEVEL are obeyed.
    const prefixedMessage = `[${this.name}] ${message}`;

    switch (level) {
      case 'error':
        defaultLogger.error(prefixedMessage);
        break;
      case 'warn':
        defaultLogger.warn(prefixedMessage);
        break;
      // Stage 'info' messages are internal pipeline progress/timing info.
      // They were previously only visible in debug mode (app.debug=true), so
      // we map them to debug level to preserve that backward-compatible behavior.
      // Use --log-level debug (or COPYTREE_LOG_LEVEL=debug) to see them.
      case 'info':
        defaultLogger.debug(p
Stage.emitProgress method · javascript · L200-L209 (10 LOC)
src/pipeline/Stage.js
  emitProgress(progress, message) {
    if (this.pipeline) {
      this.pipeline.emit('stage:progress', {
        stage: this.name,
        progress,
        message,
        timestamp: Date.now(),
      });
    }
  }
Stage.emitFileEvent method · javascript · L216-L233 (18 LOC)
src/pipeline/Stage.js
  emitFileEvent(filePath, action = 'processed') {
    if (!this.pipeline) return;

    this.fileEventCount++;
    const now = Date.now();

    // Only emit every 20th file or every 100ms
    if (this.fileEventCount % 20 === 0 || now - this.lastFileEventTime > 100) {
      this.pipeline.emit('file:batch', {
        stage: this.name,
        count: this.fileEventCount,
        lastFile: filePath,
        action,
        timestamp: now,
      });
      this.lastFileEventTime = now;
    }
  }
Methodology: Repobility · https://repobility.com/research/state-of-ai-code-2026/
Stage.getElapsedTime method · javascript · L240-L250 (11 LOC)
src/pipeline/Stage.js
  getElapsedTime(startTime) {
    const elapsed = Date.now() - startTime;

    if (elapsed < 1000) {
      return `${elapsed}ms`;
    } else if (elapsed < 60000) {
      return `${(elapsed / 1000).toFixed(1)}s`;
    } else {
      return `${(elapsed / 60000).toFixed(1)}m`;
    }
  }
Stage.formatBytes method · javascript · L257-L265 (9 LOC)
src/pipeline/Stage.js
  formatBytes(bytes) {
    if (bytes === 0) return '0 B';

    const k = 1024;
    const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));

    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  }
AlwaysIncludeStage.process method · javascript · L19-L59 (41 LOC)
src/pipeline/stages/AlwaysIncludeStage.js
  async process(input) {
    const { files } = input;

    if (!this.alwaysPatterns || this.alwaysPatterns.length === 0) {
      this.log('No always-include patterns configured', 'debug');
      return input;
    }

    const startTime = Date.now();
    let alwaysIncludeCount = 0;

    this.log(`Processing ${this.alwaysPatterns.length} always-include pattern(s)`, 'info');

    // Mark files that match always patterns
    const processedFiles = files.map((file) => {
      if (this.matchesAlwaysPatterns(file)) {
        alwaysIncludeCount++;

        logger.debug('File marked as always-include', {
          path: file.path || file.relativePath,
          patterns: this.alwaysPatterns,
        });

        return {
          ...file,
          alwaysInclude: true,
        };
      }

      return file;
    });

    const elapsed = this.getElapsedTime(startTime);
    this.log(`Marked ${alwaysIncludeCount} file(s) as always-include in ${elapsed}`, 'info');

    return {
      ...input,
    
AlwaysIncludeStage.matchesAlwaysPatterns method · javascript · L64-L94 (31 LOC)
src/pipeline/stages/AlwaysIncludeStage.js
  matchesAlwaysPatterns(file) {
    const filePath = file.path || file.relativePath;

    for (const pattern of this.alwaysPatterns) {
      // Support both glob patterns and exact matches
      if (pattern.includes('*') || pattern.includes('?') || pattern.includes('[')) {
        // Glob pattern
        if (
          minimatch(filePath, pattern, {
            matchBase: true,
            dot: true,
            nocase: process.platform === 'win32',
          })
        ) {
          return true;
        }
      } else {
        // Exact match or path contains
        if (filePath === pattern || filePath.includes(pattern)) {
          return true;
        }

        // Also check basename for convenience
        if (path.basename(filePath) === pattern) {
          return true;
        }
      }
    }

    return false;
  }
AlwaysIncludeStage.validate method · javascript · L99-L109 (11 LOC)
src/pipeline/stages/AlwaysIncludeStage.js
  validate(input) {
    if (!input || typeof input !== 'object') {
      throw new Error('Input must be an object');
    }

    if (!Array.isArray(input.files)) {
      throw new Error('Input must have a files array');
    }

    return true;
  }
CharLimitStage.process method · javascript · L9-L67 (59 LOC)
src/pipeline/stages/CharLimitStage.js
  async process(input) {
    this.log(`Applying character limit of ${this.limit.toLocaleString()} characters`, 'debug');
    const startTime = Date.now();

    let totalChars = 0;
    const limitedFiles = [];
    let truncatedFiles = 0;
    let skippedFiles = 0;

    for (const file of input.files) {
      if (!file || !file.content) {
        limitedFiles.push(file);
        continue;
      }

      const contentLength = file.content.length;

      if (totalChars + contentLength <= this.limit) {
        // File fits entirely
        limitedFiles.push(file);
        totalChars += contentLength;
      } else if (totalChars < this.limit) {
        // File partially fits
        const remainingChars = this.limit - totalChars;
        const truncatedContent = file.content.substring(0, remainingChars);

        limitedFiles.push({
          ...file,
          content: truncatedContent + '\n\n... truncated due to character limit ...',
          originalLength: contentLength,
          trunca
DeduplicateFilesStage.process method · javascript · L27-L99 (73 LOC)
src/pipeline/stages/DeduplicateFilesStage.js
  async process(files, context) {
    if (!files || files.length === 0) {
      return files;
    }

    const startTime = Date.now();
    const contentHashes = new Map();
    const duplicates = [];
    const uniqueFiles = [];

    this.log(`Checking ${files.length} files for duplicates`, 'info');

    for (const file of files) {
      // Skip files without content
      if (!file.content && file.content !== '') {
        uniqueFiles.push(file);
        continue;
      }

      // Calculate content hash
      const hash = this.calculateHash(file.content);

      if (contentHashes.has(hash)) {
        // Found a duplicate
        const original = contentHashes.get(hash);
        duplicates.push({
          file: file.path || file.relativePath,
          duplicateOf: original.path || original.relativePath,
          size: file.size || file.stats?.size || 0,
        });

        // Emit deduplication event
        if (context && context.emit) {
          context.emit('file:deduplicated', 
DeduplicateFilesStage.calculateHash method · javascript · L104-L117 (14 LOC)
src/pipeline/stages/DeduplicateFilesStage.js
  calculateHash(content) {
    const hash = crypto.createHash(this.hashAlgorithm);

    if (Buffer.isBuffer(content)) {
      hash.update(content);
    } else if (typeof content === 'string') {
      hash.update(content, 'utf8');
    } else {
      // For other types, convert to string
      hash.update(String(content), 'utf8');
    }

    return hash.digest('hex');
  }
Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
DeduplicateFilesStage.validate method · javascript · L129-L139 (11 LOC)
src/pipeline/stages/DeduplicateFilesStage.js
  validate(input) {
    if (!input || typeof input !== 'object') {
      throw new Error('Input must be an object');
    }

    if (!Array.isArray(input.files)) {
      throw new Error('Input must have a files array');
    }

    return true;
  }
FileDiscoveryStage.constructor method · javascript · L23-L42 (20 LOC)
src/pipeline/stages/FileDiscoveryStage.js
  constructor(options = {}) {
    super(options);
    this.basePath = options.basePath || process.cwd();
    this.patterns = options.patterns || ['**/*'];
    this.respectGitignore = options.respectGitignore !== false;
    this.forceInclude = options.forceInclude || [];
    this.excludes = options.excludes || [];
    // Convenience filter options
    this.extFilter = options.extFilter || null; // e.g. ['.js', '.ts']
    this.maxDepth =
      options.maxDepth !== undefined && options.maxDepth !== null ? options.maxDepth : null;
    this.minSizeBytes =
      options.minSizeBytes !== undefined && options.minSizeBytes !== null
        ? options.minSizeBytes
        : null;
    this.maxSizeBytes =
      options.maxSizeBytes !== undefined && options.maxSizeBytes !== null
        ? options.maxSizeBytes
        : null;
  }
FileDiscoveryStage.loadGitignore method · javascript · L290-L314 (25 LOC)
src/pipeline/stages/FileDiscoveryStage.js
  async loadGitignore() {
    const gitignorePath = path.join(this.basePath, '.gitignore');

    if (await fs.pathExists(gitignorePath)) {
      try {
        const gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
        // Strip BOM if present
        const cleaned =
          gitignoreContent.charCodeAt(0) === 0xfeff ? gitignoreContent.slice(1) : gitignoreContent;

        const ig = ignore().add(cleaned);
        this.log('Loaded .gitignore rules', 'debug');

        return {
          base: this.basePath,
          ig,
        };
      } catch (error) {
        this.log(`Error loading .gitignore: ${error.message}`, 'debug');
        return null;
      }
    }

    return null;
  }
FileDiscoveryStage.loadCopytreeInclude method · javascript · L319-L361 (43 LOC)
src/pipeline/stages/FileDiscoveryStage.js
  async loadCopytreeInclude() {
    const copytreeincludePath = path.join(this.basePath, '.copytreeinclude');

    if (await fs.pathExists(copytreeincludePath)) {
      const copytreeincludeContent = await fs.readFile(copytreeincludePath, 'utf8');
      const rawPatterns = copytreeincludeContent
        .split('\n')
        .map((line) => line.trim())
        .filter((line) => line && !line.startsWith('#'));

      // Transform patterns following gitignore conventions
      const transformedPatterns = rawPatterns.map((pattern) => {
        // If pattern already has glob characters, keep it as-is
        if (pattern.includes('*') || pattern.includes('?') || pattern.includes('[')) {
          return pattern;
        }

        // If pattern doesn't start with /, it matches anywhere
        if (!pattern.startsWith('/')) {
          pattern = '**/' + pattern;
        } else {
          // Remove leading slash for relative matching
          pattern = pattern.substring(1);
        }

      
FileLoadingStage.constructor method · javascript · L8-L13 (6 LOC)
src/pipeline/stages/FileLoadingStage.js
  constructor(options = {}) {
    super(options);
    this.encoding = options.encoding || 'utf8';
    // Config may not be available yet in constructor, but the proxy returns defaults
    this.binaryAction = this.config.get('copytree.binaryFileAction', 'placeholder');
  }
FileLoadingStage.process method · javascript · L15-L29 (15 LOC)
src/pipeline/stages/FileLoadingStage.js
  async process(input) {
    this.log(`Loading content for ${input.files.length} files`, 'debug');
    const startTime = Date.now();

    const filesWithContent = await Promise.all(
      input.files.map((file) => this.loadFileContent(file)),
    );

    this.log(`Loaded file contents in ${this.getElapsedTime(startTime)}`, 'info');

    return {
      ...input,
      files: filesWithContent,
    };
  }
FileLoadingStage.loadFileContent method · javascript · L31-L96 (66 LOC)
src/pipeline/stages/FileLoadingStage.js
  async loadFileContent(file) {
    try {
      // 1. Check for Structure Only patterns (Token saving)
      const structureOnlyPatterns = this.config.get('copytree.structureOnlyPatterns', []);
      const isStructureOnly = structureOnlyPatterns.some((pattern) =>
        minimatch(file.path, pattern, { dot: true, nocase: process.platform === 'win32' }),
      );

      if (isStructureOnly) {
        return {
          ...file,
          content: '[Content skipped for AI context optimization]', // Token-efficient placeholder
          isBinary: true, // Treat as binary to skip line numbering/processing
          binaryCategory: 'structure-only',
        };
      }

      // 2. Use centralized binary detector
      const det = await detect(file.absolutePath, {
        sampleBytes: this.config.get('copytree.binaryDetect.sampleBytes', 8192),
        nonPrintableThreshold: this.config.get('copytree.binaryDetect.nonPrintableThreshold', 0.3),
      });

      // Get policy for this file's cat
FileLoadingStage.handleBinaryFile method · javascript · L98-L128 (31 LOC)
src/pipeline/stages/FileLoadingStage.js
  handleBinaryFile(file, det, policy) {
    switch (policy) {
      case 'skip':
        return null;

      case 'comment':
        // Return stub for formatters to emit comments
        return {
          ...file,
          content: '',
          isBinary: true,
          excluded: true,
          excludedReason: det.category || 'binary',
          binaryCategory: det.category,
          binaryName: det.name,
        };

      case 'base64':
        return this.loadBinaryAsBase64(file, det);

      case 'placeholder':
      default:
        return {
          ...file,
          content: this.config.get('copytree.binaryPlaceholderText', '[Binary file not included]'),
          isBinary: true,
          binaryCategory: det.category,
          binaryName: det.name,
        };
    }
  }
Repobility — the code-quality scanner for AI-generated software · https://repobility.com
FileLoadingStage.loadBinaryAsBase64 method · javascript · L130-L153 (24 LOC)
src/pipeline/stages/FileLoadingStage.js
  async loadBinaryAsBase64(file, det) {
    try {
      const buffer = await fs.readFile(file.absolutePath);
      const base64 = buffer.toString('base64');

      return {
        ...file,
        content: base64,
        isBinary: true,
        encoding: 'base64',
        binaryCategory: det?.category,
        binaryName: det?.name,
      };
    } catch (error) {
      return {
        ...file,
        content: `[Error loading binary file: ${error.message}]`,
        error: error.message,
        isBinary: true,
        binaryCategory: det?.category,
        binaryName: det?.name,
      };
    }
  }
GitFilterStage.constructor method · javascript · L9-L17 (9 LOC)
src/pipeline/stages/GitFilterStage.js
  constructor(options = {}) {
    super(options);
    this.basePath = options.basePath || process.cwd();
    this.modified = options.modified || false;
    this.changed = options.changed || null;
    this.includeGitStatus = options.withGitStatus || false;

    this.gitUtils = new GitUtils(this.basePath);
  }
GitFilterStage.process method · javascript · L19-L108 (90 LOC)
src/pipeline/stages/GitFilterStage.js
  async process(input) {
    // Use basePath from input if provided, otherwise fall back to constructor option
    if (input.basePath) {
      this.basePath = input.basePath;
      this.gitUtils = new GitUtils(this.basePath);
    }

    // Skip if no git filtering is requested
    if (!this.modified && !this.changed && !this.includeGitStatus) {
      return input;
    }

    this.log('Applying git filters', 'debug');
    const startTime = Date.now();

    try {
      // Check if we're in a git repository
      if (!(await this.gitUtils.isGitRepository())) {
        this.log('Not a git repository, skipping git filters', 'warn');
        return input;
      }

      let filteredFiles = input.files;
      let gitFiles = [];

      // Get the appropriate file list based on options
      if (this.modified) {
        gitFiles = await this.gitUtils.getModifiedFiles();
        this.log(`Found ${gitFiles.length} modified files`, 'debug');
      } else if (this.changed) {
        gitFiles = awai
InstructionsStage.process method · javascript · L14-L83 (70 LOC)
src/pipeline/stages/InstructionsStage.js
  async process(input) {
    const startTime = Date.now();

    // Check if instructions are disabled
    if (input.options?.noInstructions || input.options?.instructions === false) {
      this.log('Instructions disabled via --no-instructions', 'debug');
      return input;
    }

    this.log('InstructionsStage processing started', 'debug');

    try {
      // Determine which instructions to load
      let instructionsName = input.options?.instructions;

      // If instructions is true but not a string, use default
      if (instructionsName === true || !instructionsName) {
        instructionsName = this.config.get('app.defaultInstructions', 'default');
      }

      this.log(`Loading instructions: ${instructionsName}`, 'debug');

      // Load instructions
      this.log(`Loading instructions: ${instructionsName}`, 'debug');
      let instructionsContent;
      try {
        instructionsContent = await this.instructionsLoader.load(instructionsName);
      } catch (error) {
     
InstructionsStage.validate method · javascript · L90-L112 (23 LOC)
src/pipeline/stages/InstructionsStage.js
  async validate(input) {
    // Skip validation if instructions are disabled
    if (input.options?.noInstructions || input.options?.instructions === false) {
      return true;
    }

    let instructionsName = input.options?.instructions;

    // If instructions is true but not a string, use default
    if (instructionsName === true || !instructionsName) {
      instructionsName = this.config.get('app.defaultInstructions', 'default');
    }

    // Check if instructions exist
    const exists = await this.instructionsLoader.exists(instructionsName);

    // Only throw error if a specific instructions set was requested (not true or default)
    if (!exists && input.options?.instructions && input.options.instructions !== true) {
      throw new Error(`Instructions '${instructionsName}' not found`);
    }

    return true;
  }
LimitStage.process method · javascript · L9-L30 (22 LOC)
src/pipeline/stages/LimitStage.js
  async process(input) {
    this.log(`Limiting to first ${this.limit} files`, 'debug');

    if (input.files.length <= this.limit) {
      return input;
    }

    const limitedFiles = input.files.slice(0, this.limit);
    const truncatedCount = input.files.length - this.limit;

    this.log(`Limited output to ${this.limit} files (${truncatedCount} truncated)`, 'info');

    return {
      ...input,
      files: limitedFiles,
      stats: {
        ...input.stats,
        truncated: true,
        truncatedCount,
      },
    };
  }
OutputFormattingStage.constructor method · javascript · L8-L19 (12 LOC)
src/pipeline/stages/OutputFormattingStage.js
  constructor(options = {}) {
    super(options);
    // Normalize and default format (xml is default)
    const raw = (options.format || 'xml').toString().toLowerCase();
    this.format = raw === 'md' ? 'markdown' : raw;
    this.addLineNumbers =
      options.addLineNumbers ??
      options.withLineNumbers ??
      this.config.get('copytree.addLineNumbers', false);
    this.lineNumberFormat = this.config.get('copytree.lineNumberFormat', '%4d: ');
    this.onlyTree = options.onlyTree || false;
  }
OutputFormattingStage.handleError method · javascript · L24-L33 (10 LOC)
src/pipeline/stages/OutputFormattingStage.js
  async handleError(error, input) {
    this.log(`Output formatting failed: ${error.message}, returning raw data`, 'warn');
    // Return a minimal valid output structure
    return {
      ...input,
      output: JSON.stringify({ error: error.message, files: input.files || [] }),
      outputFormat: 'json',
      outputSize: 0,
    };
  }
Open data scored by Repobility · https://repobility.com
OutputFormattingStage.process method · javascript · L35-L95 (61 LOC)
src/pipeline/stages/OutputFormattingStage.js
  async process(input) {
    this.log(`Formatting output as ${this.format}`, 'debug');
    const startTime = Date.now();

    let output;
    switch (this.format) {
      case 'xml': {
        const formatter = new XMLFormatter({
          stage: this,
          addLineNumbers: this.addLineNumbers,
          onlyTree: this.onlyTree,
        });
        output = await formatter.format(input);
        break;
      }
      case 'json':
        output = this.formatAsJSON(input);
        break;
      case 'tree':
        output = this.formatAsTree(input);
        break;
      case 'markdown': {
        const formatter = new MarkdownFormatter({
          stage: this,
          addLineNumbers: this.addLineNumbers,
          onlyTree: this.onlyTree,
        });
        output = await formatter.format(input);
        break;
      }
      case 'ndjson': {
        const formatter = new NDJSONFormatter({
          stage: this,
          addLineNumbers: this.addLineNumbers,
          onlyTree: this
OutputFormattingStage.formatAsJSON method · javascript · L97-L135 (39 LOC)
src/pipeline/stages/OutputFormattingStage.js
  formatAsJSON(input) {
    const output = {
      directory: input.basePath,
      metadata: {
        generated: new Date().toISOString(),
        fileCount: input.files.filter((f) => f !== null).length,
        totalSize: this.calculateTotalSize(input.files),
        profile: input.profile?.name || 'default',
        directoryStructure: this.generateDirectoryStructure(input.files),
        ...(input.instructions && {
          instructions: input.instructions,
        }),
      },
      files: input.files
        .filter((f) => f !== null)
        .map((file) => {
          const fileObj = {
            path: file.path,
            size: file.size,
            modified: file.modified,
            isBinary: file.isBinary,
            encoding: file.encoding,
          };

          // Add content unless --only-tree is set
          if (!this.onlyTree) {
            fileObj.content =
              this.addLineNumbers && !file.isBinary
                ? this.addLineNumbersToContent(fil
OutputFormattingStage.formatAsTree method · javascript · L137-L159 (23 LOC)
src/pipeline/stages/OutputFormattingStage.js
  formatAsTree(input) {
    const lines = [];
    // Tree formatting options (currently using defaults)
    // const indent = this.config.get('copytree.treeIndent', '  ');
    // const connectors = this.config.get('copytree.treeConnectors');

    lines.push(input.basePath);
    lines.push('');

    // Build tree structure
    const tree = this.buildTreeStructure(input.files);

    // Render tree
    this.renderTree(tree, lines, '', true);

    // Add summary
    lines.push('');
    lines.push(
      `${input.files.filter((f) => f !== null).length} files, ${this.formatBytes(this.calculateTotalSize(input.files))}`,
    );

    return lines.join('\n');
  }
‹ prevpage 2 / 7next ›