import { isBundledSEA } from './chunk-KPFLSGGP.js';
import { detectJsPackageManager, logLines, fmt } from './chunk-NEWKASA7.js';
import { gracefulExit } from './chunk-XQSWW35O.js';
import { define } from './chunk-WIL4LMMA.js';
import { loadVarlockEnvGraph } from './chunk-5SI4EGRL.js';
import { CliExitError } from './chunk-7M6WZ65V.js';
import { spawnAsync, pathExists, ansis_default } from './chunk-LTYLEVHC.js';
import { my_dash_default } from './chunk-DRL5U3PK.js';
import { redactString } from './chunk-UWUBFNLX.js';
import { __name } from './chunk-IBAHCH6O.js';
import path from 'path';
import fs from 'fs/promises';

var SKIP_DIRS = /* @__PURE__ */ new Set([
  ".git",
  "node_modules",
  "dist",
  "build",
  ".next",
  ".nuxt",
  "vendor",
  ".venv",
  "venv",
  "__pycache__",
  ".turbo",
  "coverage"
]);
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
  ".png",
  ".jpg",
  ".jpeg",
  ".gif",
  ".bmp",
  ".ico",
  ".mp3",
  ".mp4",
  ".wav",
  ".avi",
  ".mov",
  ".zip",
  ".tar",
  ".gz",
  ".rar",
  ".7z",
  ".exe",
  ".dll",
  ".so",
  ".dylib",
  ".bin",
  ".pdf",
  ".doc",
  ".docx",
  ".xls",
  ".xlsx",
  ".woff",
  ".woff2",
  ".ttf",
  ".eot",
  ".pyc",
  ".class",
  ".o"
]);
var commandSpec = define({
  name: "scan",
  description: "Scan files for sensitive config values that should not be in plaintext",
  args: {
    staged: {
      type: "boolean",
      description: "Only scan staged git files"
    },
    "include-ignored": {
      type: "boolean",
      description: "Include git-ignored files in the scan"
    },
    "install-hook": {
      type: "boolean",
      description: "Set up varlock scan as a git pre-commit hook"
    },
    path: {
      type: "string",
      short: "p",
      multiple: true,
      description: 'Path to a specific .env file (e.g. .env.prod) or directory ending with "/" to use as the schema entry point (can be specified multiple times)'
    }
  },
  examples: `
Loads your varlock config, resolves all sensitive values, then scans files to
ensure none of those sensitive values appear in plaintext.

Examples:
  varlock scan                    # Scan non-git-ignored files in current directory
  varlock scan --staged           # Only scan staged git files
  varlock scan --include-ignored  # Scan all files, including git-ignored ones
  varlock scan --path .env.prod   # Use a specific .env file as the schema entry point
  varlock scan -p ./envs -p ./overrides  # Use multiple schema entry points
  varlock scan --install-hook     # Set up as a git pre-commit hook
  varlock scan ./dist             # Scan a specific directory (e.g. a build output folder)
  varlock scan ./dist ./public    # Scan multiple directories
  varlock scan './dist/**/*.js'   # Scan files matching a glob pattern
  `.trim()
});
async function getGitFiles(cwd, onlyStaged) {
  try {
    let output;
    if (onlyStaged) {
      output = await spawnAsync(
        "git",
        ["diff", "--cached", "--name-only", "-z", "--diff-filter=ACMR"],
        { cwd }
      );
    } else {
      output = await spawnAsync(
        "git",
        ["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
        { cwd }
      );
    }
    return output.split("\0").filter(Boolean).filter((f) => !BINARY_EXTENSIONS.has(path.extname(f).toLowerCase())).map((f) => path.resolve(cwd, f));
  } catch {
    return null;
  }
}
__name(getGitFiles, "getGitFiles");
async function walkDirectory(dir) {
  const files = [];
  let entries;
  try {
    entries = await fs.readdir(dir, { withFileTypes: true });
  } catch {
    return files;
  }
  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      if (SKIP_DIRS.has(entry.name)) continue;
      files.push(...await walkDirectory(fullPath));
    } else if (entry.isFile()) {
      const ext = path.extname(entry.name).toLowerCase();
      if (BINARY_EXTENSIONS.has(ext)) continue;
      files.push(fullPath);
    }
  }
  return files;
}
__name(walkDirectory, "walkDirectory");
async function walkDirectoryAll(dir) {
  const files = [];
  let entries;
  try {
    entries = await fs.readdir(dir, { withFileTypes: true });
  } catch {
    return files;
  }
  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      files.push(...await walkDirectoryAll(fullPath));
    } else if (entry.isFile()) {
      const ext = path.extname(entry.name).toLowerCase();
      if (BINARY_EXTENSIONS.has(ext)) continue;
      files.push(fullPath);
    }
  }
  return files;
}
__name(walkDirectoryAll, "walkDirectoryAll");
var GLOB_CHARS = /[*?{}[\]]/;
async function resolveTargetPaths(targets, cwd) {
  const seen = /* @__PURE__ */ new Set();
  const files = [];
  async function addFile(absPath) {
    if (seen.has(absPath)) return;
    seen.add(absPath);
    files.push(absPath);
  }
  __name(addFile, "addFile");
  async function addPath(absPath) {
    let stat;
    try {
      stat = await fs.stat(absPath);
    } catch {
      return;
    }
    if (stat.isDirectory()) {
      for (const f of await walkDirectoryAll(absPath)) {
        await addFile(f);
      }
    } else if (stat.isFile()) {
      const ext = path.extname(absPath).toLowerCase();
      if (!BINARY_EXTENSIONS.has(ext)) {
        await addFile(absPath);
      }
    }
  }
  __name(addPath, "addPath");
  for (const target of targets) {
    if (GLOB_CHARS.test(target)) {
      for await (const match of fs.glob(target, { cwd })) {
        await addPath(path.resolve(cwd, match));
      }
    } else {
      await addPath(path.resolve(cwd, target));
    }
  }
  return files;
}
__name(resolveTargetPaths, "resolveTargetPaths");
async function scanFileForValues(filePath, sensitiveValues) {
  const findings = [];
  let content;
  try {
    content = await fs.readFile(filePath, "utf-8");
  } catch {
    return findings;
  }
  if (content.includes("\0")) return findings;
  let anyMatch = false;
  for (const val of sensitiveValues.values()) {
    if (content.includes(val)) {
      anyMatch = true;
      break;
    }
  }
  if (!anyMatch) return findings;
  const lines = content.split("\n");
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    for (const [keyName, val] of sensitiveValues) {
      const colIdx = line.indexOf(val);
      if (colIdx !== -1) {
        findings.push({
          filePath,
          lineNumber: i + 1,
          columnNumber: colIdx + 1,
          line: line.trim(),
          sensitiveKeyName: keyName,
          sensitiveValue: val
        });
        break;
      }
    }
  }
  return findings;
}
__name(scanFileForValues, "scanFileForValues");
var SCAN_COMMAND = "varlock scan";
function getHookCommand() {
  if (isBundledSEA()) return SCAN_COMMAND;
  const pm = detectJsPackageManager();
  if (pm) return `${pm.exec} ${SCAN_COMMAND}`;
  return SCAN_COMMAND;
}
__name(getHookCommand, "getHookCommand");
async function detectHookManager(cwd) {
  if (await pathExists(path.join(cwd, ".husky"))) return "husky";
  const lefthookFiles = ["lefthook.yml", "lefthook.yaml", ".lefthook.yml", ".lefthook.yaml"];
  for (const file of lefthookFiles) {
    if (await pathExists(path.join(cwd, file))) return "lefthook";
  }
  const pkgJsonPath = path.join(cwd, "package.json");
  if (await pathExists(pkgJsonPath)) {
    try {
      const pkgJson = JSON.parse(await fs.readFile(pkgJsonPath, "utf-8"));
      if (pkgJson["simple-git-hooks"]) return "simple-git-hooks";
    } catch {
    }
  }
  return null;
}
__name(detectHookManager, "detectHookManager");
async function findGitRoot(cwd) {
  try {
    const root = await spawnAsync("git", ["rev-parse", "--show-toplevel"], { cwd });
    return root.trim();
  } catch {
    return null;
  }
}
__name(findGitRoot, "findGitRoot");
async function installHook(cwd) {
  const gitRoot = await findGitRoot(cwd);
  if (!gitRoot) {
    throw new CliExitError("Not inside a git repository", {
      suggestion: "Run `git init` first, or make sure you are inside a git repository."
    });
  }
  const hookCommand = getHookCommand();
  const hookScript = `#!/bin/sh
${hookCommand}
`;
  const hookManager = await detectHookManager(gitRoot);
  if (hookManager === "husky") {
    logLines([
      "",
      `Detected ${ansis_default.bold("husky")} as your git hook manager.`,
      "",
      `Add ${ansis_default.bold(hookCommand)} to your pre-commit hook:`,
      "",
      ansis_default.dim(`  echo "${hookCommand}" >> .husky/pre-commit`),
      "",
      "Or if creating a new hook:",
      "",
      ansis_default.dim(`  echo "${hookCommand}" > .husky/pre-commit`),
      ""
    ]);
    return;
  }
  if (hookManager === "lefthook") {
    logLines([
      "",
      `Detected ${ansis_default.bold("lefthook")} as your git hook manager.`,
      "",
      "Add the following to your lefthook config:",
      "",
      ansis_default.dim("  pre-commit:"),
      ansis_default.dim("    commands:"),
      ansis_default.dim("      varlock-scan:"),
      ansis_default.dim(`        run: ${hookCommand}`),
      ""
    ]);
    return;
  }
  if (hookManager === "simple-git-hooks") {
    logLines([
      "",
      `Detected ${ansis_default.bold("simple-git-hooks")} in your package.json.`,
      "",
      "Add the following to your package.json:",
      "",
      ansis_default.dim('  "simple-git-hooks": {'),
      ansis_default.dim(`    "pre-commit": "${hookCommand}"`),
      ansis_default.dim("  }"),
      "",
      `Then run ${ansis_default.dim("npx simple-git-hooks")} to update the hooks.`,
      ""
    ]);
    return;
  }
  const hooksDir = path.join(gitRoot, ".git", "hooks");
  const hookPath = path.join(hooksDir, "pre-commit");
  await fs.mkdir(hooksDir, { recursive: true });
  if (await pathExists(hookPath)) {
    const existingContent = await fs.readFile(hookPath, "utf-8");
    if (existingContent.includes(SCAN_COMMAND)) {
      logLines([
        "",
        ansis_default.green(`The pre-commit hook already includes ${ansis_default.bold(SCAN_COMMAND)} - nothing to do!`),
        ""
      ]);
      return;
    }
    const updatedContent = `${existingContent.trimEnd()}
${hookCommand}
`;
    await fs.writeFile(hookPath, updatedContent);
    await fs.chmod(hookPath, 493);
    logLines([
      "",
      ansis_default.green(`Added ${ansis_default.bold(hookCommand)} to existing pre-commit hook.`),
      fmt.filePath(hookPath),
      ""
    ]);
    return;
  }
  await fs.writeFile(hookPath, hookScript);
  await fs.chmod(hookPath, 493);
  logLines([
    "",
    ansis_default.green(`Created pre-commit hook at ${fmt.filePath(hookPath)}`),
    ansis_default.dim("Your staged files will now be scanned for sensitive values before each commit."),
    ""
  ]);
}
__name(installHook, "installHook");
var commandFn = /* @__PURE__ */ __name(async (ctx) => {
  if (ctx.values["install-hook"]) {
    await installHook(process.cwd());
    return;
  }
  const onlyStaged = ctx.values.staged ?? false;
  const includeIgnored = ctx.values["include-ignored"] ?? false;
  const scanTargets = (ctx.positionals ?? []).slice(ctx.commandPath?.length ?? 0);
  const envGraph = await loadVarlockEnvGraph({
    entryFilePaths: ctx.values.path
  });
  for (const source of envGraph.sortedDataSources) {
    if (source.loadingError) {
      throw new CliExitError(`Error loading config: ${source.loadingError.message}`, {
        suggestion: "Make sure your .env.schema file is valid."
      });
    }
  }
  await envGraph.resolveEnvValues();
  const sensitiveValues = /* @__PURE__ */ new Map();
  for (const itemKey in envGraph.configSchema) {
    const item = envGraph.configSchema[itemKey];
    if (item.isSensitive && my_dash_default.isString(item.resolvedValue) && item.resolvedValue !== "") {
      sensitiveValues.set(itemKey, item.resolvedValue);
    }
  }
  if (sensitiveValues.size === 0) {
    logLines([ansis_default.green("\u2705 No sensitive values found in config - nothing to scan for.")]);
    return;
  }
  const cwd = process.cwd();
  let files;
  if (scanTargets.length > 0) {
    files = await resolveTargetPaths(scanTargets, cwd);
  } else if (includeIgnored) {
    files = await walkDirectory(cwd);
  } else {
    const gitFiles = await getGitFiles(cwd, onlyStaged);
    if (gitFiles !== null) {
      files = gitFiles;
    } else {
      if (onlyStaged) {
        throw new CliExitError("Could not run git to find staged files", {
          suggestion: "Make sure git is installed and you are inside a git repository."
        });
      }
      files = await walkDirectory(cwd);
    }
  }
  if (files.length === 0) {
    if (scanTargets.length > 0) {
      console.log(ansis_default.green("\u2705 No files found at the specified path(s)."));
    } else if (onlyStaged) {
      console.log("No staged files to scan.");
    } else {
      console.log(ansis_default.green("\u2705 No files found to scan."));
    }
    return;
  }
  const allFindings = [];
  for (const filePath of files) {
    const findings = await scanFileForValues(filePath, sensitiveValues);
    allFindings.push(...findings);
  }
  if (allFindings.length === 0) {
    logLines([ansis_default.green(`\u2705 No sensitive values found in plaintext. (scanned ${files.length} file${files.length === 1 ? "" : "s"})`)]);
    return;
  }
  const findingsByFile = /* @__PURE__ */ new Map();
  for (const finding of allFindings) {
    const existing = findingsByFile.get(finding.filePath) ?? [];
    existing.push(finding);
    findingsByFile.set(finding.filePath, existing);
  }
  console.error(ansis_default.red(`
\u{1F6A8} Found ${allFindings.length} sensitive value(s) in plaintext across ${findingsByFile.size} file(s):
`));
  for (const [filePath, findings] of findingsByFile) {
    const relPath = path.relative(cwd, filePath);
    for (const finding of findings) {
      const redactedLine = finding.line.replaceAll(finding.sensitiveValue, redactString(finding.sensitiveValue));
      const truncatedLine = redactedLine.length > 100 ? `${redactedLine.substring(0, 100)}\u2026` : redactedLine;
      console.error(`  ${fmt.fileName(`${relPath}:${finding.lineNumber}:${finding.columnNumber}`)} ${ansis_default.yellow(finding.sensitiveKeyName)}`);
      console.error(`    ${ansis_default.dim(truncatedLine)}`);
    }
    console.error("");
  }
  gracefulExit(1);
}, "commandFn");

export { commandFn, commandSpec, getGitFiles, resolveTargetPaths, scanFileForValues, walkDirectory, walkDirectoryAll };
//# sourceMappingURL=chunk-SIXH3GKD.js.map
//# sourceMappingURL=chunk-SIXH3GKD.js.map