{"version":3,"file":"impl-7pxeoDZg.mjs","names":["fg"],"sources":["../src/commands/admin/find-text-in-folder/impl.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport colors from 'colors';\nimport fg from 'fast-glob';\n\nimport type { LocalContext } from '../../../context.js';\nimport { doneInputValidation } from '../../../lib/cli/done-input-validation.js';\nimport { logger } from '../../../logger.js';\n\n/** CLI flags accepted by the `find-text-in-folder` command. */\nexport type FindTextInFolderCommandFlags = {\n  /** The text string to search for */\n  needle: string;\n  /** Root directory to search */\n  root: string;\n  /** Comma-separated file extensions */\n  exts: string;\n  /** Skip parquet file scanning */\n  noParquet: boolean;\n  /** Max concurrent file scans */\n  concurrency: number;\n  /** Stop scanning each file after N bytes */\n  maxBytes?: number;\n};\n\n/**\n * Streams through a file checking if it contains the needle (case-insensitive).\n *\n * @param filePath - Absolute path to the file to scan\n * @param needle - Lowercased needle as a Buffer\n * @param maxBytes - Optional byte limit per file\n * @returns Whether the file contains the needle\n */\nexport function fileContainsExactBytes(\n  filePath: string,\n  needle: Buffer,\n  maxBytes?: number,\n): Promise<boolean> {\n  return new Promise<boolean>((resolve, reject) => {\n    const stream = fs.createReadStream(filePath);\n    let carry = Buffer.alloc(0);\n    const n = needle.length;\n    let seen = 0;\n\n    stream.on('data', (raw) => {\n      let chunk = typeof raw === 'string' ? Buffer.from(raw) : raw;\n\n      if (maxBytes) {\n        const remaining = maxBytes - seen;\n        if (remaining <= 0) {\n          stream.destroy();\n          resolve(false);\n          return;\n        }\n        if (chunk.length > remaining) {\n          chunk = chunk.subarray(0, remaining);\n        }\n        seen += chunk.length;\n      }\n\n      const buf = carry.length ? Buffer.concat([carry, chunk]) : chunk;\n      const haystack = buf.toString('utf8').toLowerCase();\n      if (haystack.includes(needle.toString('utf8'))) {\n        stream.destroy();\n        resolve(true);\n        return;\n      }\n\n      // Keep last n-1 bytes to catch boundary matches\n      if (n > 1) {\n        carry = Buffer.from(buf.subarray(Math.max(0, buf.length - (n - 1))));\n      } else {\n        carry = Buffer.alloc(0);\n      }\n    });\n\n    stream.on('error', reject);\n    stream.on('close', () => resolve(false));\n    stream.on('end', () => resolve(false));\n  });\n}\n\n/**\n * Run async workers over items with bounded concurrency.\n *\n * @param items - Array of items to process\n * @param limit - Maximum concurrent workers\n * @param worker - Async function to run per item\n * @returns Resolves when all items are processed\n */\nasync function runWithConcurrency<T>(\n  items: T[],\n  limit: number,\n  worker: (item: T) => Promise<void>,\n): Promise<void> {\n  let idx = 0;\n  const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {\n    // eslint-disable-next-line no-constant-condition\n    while (true) {\n      const current = idx;\n      idx += 1;\n      if (current >= items.length) return;\n      await worker(items[current]);\n    }\n  });\n  await Promise.all(runners);\n}\n\n/**\n * Execute a DuckDB query and return stdout.\n *\n * @param duckdbPath - Path to the duckdb binary\n * @param sql - SQL query to execute\n * @returns The stdout output from duckdb\n */\nfunction duckdbQuery(duckdbPath: string, sql: string): Promise<string> {\n  return new Promise<string>((resolve, reject) => {\n    const child = spawn(duckdbPath, ['-noheader', '-batch', '-cmd', sql], {\n      stdio: ['ignore', 'pipe', 'pipe'],\n    });\n\n    let stdout = '';\n    let stderr = '';\n    child.stdout.on('data', (d) => {\n      stdout += String(d);\n    });\n    child.stderr.on('data', (d) => {\n      stderr += String(d);\n    });\n\n    child.on('error', reject);\n    child.on('close', (code) => {\n      if (code === 0) resolve(stdout);\n      else reject(new Error(`duckdb exited ${code}: ${stderr}`));\n    });\n  });\n}\n\n/**\n * Get all VARCHAR/STRING column names from a parquet file.\n *\n * @param duckdbPath - Path to the duckdb binary\n * @param filePath - Absolute path to the parquet file\n * @returns Array of string column names\n */\nasync function duckdbGetParquetStringColumns(\n  duckdbPath: string,\n  filePath: string,\n): Promise<string[]> {\n  const escaped = filePath.replace(/'/g, \"''\");\n  const sql = [\n    'SELECT column_name',\n    `FROM parquet_schema('${escaped}')`,\n    \"WHERE lower(column_type) LIKE '%varchar%'\",\n    \"   OR lower(column_type) LIKE '%string%';\",\n  ].join('\\n');\n\n  const out = await duckdbQuery(duckdbPath, sql);\n  return out\n    .split('\\n')\n    .map((l) => l.trim())\n    .filter(Boolean);\n}\n\n/**\n * Check if any string column in a parquet file contains the needle value.\n *\n * @param duckdbPath - Path to the duckdb binary\n * @param filePath - Absolute path to the parquet file\n * @param needle - The string to search for (exact equality per column)\n * @returns Whether any row/column matches\n */\nasync function parquetFileHasExactString(\n  duckdbPath: string,\n  filePath: string,\n  needle: string,\n): Promise<boolean> {\n  const cols = await duckdbGetParquetStringColumns(duckdbPath, filePath);\n  if (cols.length === 0) return false;\n\n  const escaped = filePath.replace(/'/g, \"''\");\n  const orChain = cols\n    .map((c) => `\"${c.replace(/\"/g, '\"\"')}\" = '${needle.replace(/'/g, \"''\")}'`)\n    .join(' OR ');\n\n  const sql = [\n    `SELECT 1 AS hit FROM read_parquet('${escaped}')`,\n    `WHERE ${orChain}`,\n    'LIMIT 1;',\n  ].join('\\n');\n\n  const out = await duckdbQuery(duckdbPath, sql);\n  return out.trim().length > 0;\n}\n\n/**\n * Entrypoint for the `admin find-text-in-folder` command.\n *\n * Searches a folder of files for a given text string. Useful for finding\n * a needle in a haystack across many large files (multi-GB CSVs, JSON\n * dumps, log archives). Files are streamed so memory stays flat.\n *\n * @param this - Bound CLI context\n * @param flags - CLI flags for the run\n */\nexport async function findTextInFolder(\n  this: LocalContext,\n  flags: FindTextInFolderCommandFlags,\n): Promise<void> {\n  doneInputValidation(this.process.exit);\n\n  const { needle, root, exts, noParquet, concurrency, maxBytes } = flags;\n  const rootAbs = path.resolve(root);\n\n  const extSet = new Set(\n    exts\n      .split(',')\n      .map((x) => x.trim().replace(/^\\./, '').toLowerCase())\n      .filter(Boolean),\n  );\n  const patterns = Array.from(extSet).map((e) => `**/*.${e}`);\n\n  logger.info(\n    colors.green(`Searching for \"${needle}\" in ${rootAbs} (exts: ${[...extSet].join(', ')})`),\n  );\n\n  const normalFiles = await fg(patterns, {\n    cwd: rootAbs,\n    absolute: true,\n    onlyFiles: true,\n    followSymbolicLinks: false,\n    suppressErrors: true,\n  });\n\n  const needleBuf = Buffer.from(needle.toLowerCase(), 'utf8');\n  const hits: string[] = [];\n\n  await runWithConcurrency(normalFiles, concurrency, async (file) => {\n    try {\n      const ok = await fileContainsExactBytes(file, needleBuf, maxBytes);\n      if (ok) {\n        hits.push(file);\n        this.process.stdout.write(`${file}\\n`);\n      }\n    } catch {\n      // ignore unreadable files\n    }\n  });\n\n  if (!noParquet) {\n    const parquetFiles = await fg(['**/*.parquet'], {\n      cwd: rootAbs,\n      absolute: true,\n      onlyFiles: true,\n      followSymbolicLinks: false,\n      suppressErrors: true,\n    });\n\n    if (parquetFiles.length > 0) {\n      logger.info(colors.green(`Scanning ${parquetFiles.length} parquet file(s) via DuckDB...`));\n\n      await runWithConcurrency(\n        parquetFiles,\n        Math.max(2, Math.floor(concurrency / 4)),\n        async (file) => {\n          try {\n            const ok = await parquetFileHasExactString('duckdb', file, needle);\n            if (ok) {\n              hits.push(file);\n              this.process.stdout.write(`${file}\\n`);\n            }\n          } catch {\n            // ignore parquet read issues\n          }\n        },\n      );\n    }\n  }\n\n  logger.info(colors.green(`Done. Found ${hits.length} matching file(s).`));\n}\n"],"mappings":"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"}