πŸ“„ File detail

utils/filePersistence/outputsScanner.ts

🧩 .tsπŸ“ 127 linesπŸ’Ύ 3,682 bytesπŸ“ text
← Back to All Files

🎯 Use case

This file lives under β€œutils/”, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, …). On the API surface it exposes logDebug, getEnvironmentKind, and findModifiedFiles β€” mainly functions, hooks, or classes. Dependencies touch Node filesystem and Node path helpers. It composes internal code from debug, teleport, and types (relative imports). What the file header says: Outputs directory scanner for file persistence This module provides utilities to: - Detect the session type from environment variables - Capture turn start timestamp - Find modified files by comparing file mtimes against turn start time.

Generated from folder role, exports, dependency roots, and inline comments β€” not hand-reviewed for every path.

🧠 Inline summary

Outputs directory scanner for file persistence This module provides utilities to: - Detect the session type from environment variables - Capture turn start timestamp - Find modified files by comparing file mtimes against turn start time

πŸ“€ Exports (heuristic)

  • logDebug
  • getEnvironmentKind
  • findModifiedFiles

πŸ“š External import roots

Package roots from from "…" (relative paths omitted).

  • fs
  • path

πŸ–₯️ Source preview

/**
 * Outputs directory scanner for file persistence
 *
 * This module provides utilities to:
 * - Detect the session type from environment variables
 * - Capture turn start timestamp
 * - Find modified files by comparing file mtimes against turn start time
 */

import * as fs from 'fs/promises'
import * as path from 'path'
import { logForDebugging } from '../debug.js'
import type { EnvironmentKind } from '../teleport/environments.js'
import type { TurnStartTime } from './types.js'

/** Shared debug logger for file persistence modules */
export function logDebug(message: string): void {
  logForDebugging(`[file-persistence] ${message}`)
}

/**
 * Get the environment kind from CLAUDE_CODE_ENVIRONMENT_KIND.
 * Returns null if not set or not a recognized value.
 */
export function getEnvironmentKind(): EnvironmentKind | null {
  const kind = process.env.CLAUDE_CODE_ENVIRONMENT_KIND
  if (kind === 'byoc' || kind === 'anthropic_cloud') {
    return kind
  }
  return null
}

function hasParentPath(
  entry: object,
): entry is { parentPath: string; name: string } {
  return 'parentPath' in entry && typeof entry.parentPath === 'string'
}

function hasPath(entry: object): entry is { path: string; name: string } {
  return 'path' in entry && typeof entry.path === 'string'
}

function getEntryParentPath(entry: object, fallback: string): string {
  if (hasParentPath(entry)) {
    return entry.parentPath
  }
  if (hasPath(entry)) {
    return entry.path
  }
  return fallback
}

/**
 * Find files that have been modified since the turn started.
 * Returns paths of files with mtime >= turnStartTime.
 *
 * Uses recursive directory listing and parallelized stat calls for efficiency.
 *
 * @param turnStartTime - The timestamp when the turn started
 * @param outputsDir - The directory to scan for modified files
 */
export async function findModifiedFiles(
  turnStartTime: TurnStartTime,
  outputsDir: string,
): Promise<string[]> {
  // Use recursive flag to get all entries in one call
  let entries: Awaited<ReturnType<typeof fs.readdir>>
  try {
    entries = await fs.readdir(outputsDir, {
      withFileTypes: true,
      recursive: true,
    })
  } catch {
    // Directory doesn't exist or is not accessible
    return []
  }

  // Filter to regular files only (skip symlinks for security) and build full paths
  const filePaths: string[] = []
  for (const entry of entries) {
    if (entry.isSymbolicLink()) {
      continue
    }
    if (entry.isFile()) {
      // entry.parentPath is available in Node 20+, fallback to entry.path for older versions
      const parentPath = getEntryParentPath(entry, outputsDir)
      filePaths.push(path.join(parentPath, entry.name))
    }
  }

  if (filePaths.length === 0) {
    logDebug('No files found in outputs directory')
    return []
  }

  // Parallelize stat calls for all files
  const statResults = await Promise.all(
    filePaths.map(async filePath => {
      try {
        const stat = await fs.lstat(filePath)
        // Skip if it became a symlink between readdir and stat (race condition)
        if (stat.isSymbolicLink()) {
          return null
        }
        return { filePath, mtimeMs: stat.mtimeMs }
      } catch {
        // File may have been deleted between readdir and stat
        return null
      }
    }),
  )

  // Filter to files modified since turn start
  const modifiedFiles: string[] = []
  for (const result of statResults) {
    if (result && result.mtimeMs >= turnStartTime) {
      modifiedFiles.push(result.filePath)
    }
  }

  logDebug(
    `Found ${modifiedFiles.length} modified files since turn start (scanned ${filePaths.length} total)`,
  )

  return modifiedFiles
}