Docs/Skills and code/Top-level code

dump-project.js

Code513 lines7.8 KBTop-level code

import fs from "node:fs";
import path from "node:path";

const ROOT = process.cwd();
const OUTPUT = path.join(
  ROOT,
  "project-dump.txt"
);

/*
 * Directories that should not be dumped.
 *
 * These are either generated, extremely large,
 * machine-specific, or not useful to an LLM
 * trying to understand the project.
 */
const EXCLUDED_DIRECTORIES = new Set([
  "node_modules",
  ".git",
  ".idea",
  ".vscode",
  "data",
  "dist",
  "build",
  "coverage"
]);

/*
 * Specific files to exclude.
 */
const EXCLUDED_FILES = new Set([
  "project-dump.txt",
  ".env"
]);

/*
 * Files larger than this are skipped.
 *
 * This prevents accidentally dumping huge logs,
 * generated files, or datasets.
 */
const MAX_FILE_SIZE =
  2 * 1024 * 1024;

/*
 * Extensions that are definitely binary or
 * otherwise unsuitable for an LLM project dump.
 */
const BINARY_EXTENSIONS = new Set([
  ".db",
  ".sqlite",
  ".sqlite3",
  ".png",
  ".jpg",
  ".jpeg",
  ".gif",
  ".webp",
  ".bmp",
  ".ico",
  ".mp3",
  ".wav",
  ".ogg",
  ".mp4",
  ".mov",
  ".avi",
  ".mkv",
  ".zip",
  ".gz",
  ".tar",
  ".7z",
  ".pdf",
  ".woff",
  ".woff2",
  ".ttf",
  ".otf",
  ".exe",
  ".dll"
]);

function relativePath(filePath) {
  return path
    .relative(ROOT, filePath)
    .split(path.sep)
    .join("/");
}

function shouldExcludeDirectory(name) {
  return EXCLUDED_DIRECTORIES.has(name);
}

function shouldExcludeFile(name) {
  return (
    EXCLUDED_FILES.has(name) ||
    (name.startsWith(".env.") && name !== ".env.example")
  );
}

function isBinaryExtension(filePath) {
  const extension =
    path.extname(filePath).toLowerCase();

  return BINARY_EXTENSIONS.has(
    extension
  );
}

function isProbablyBinary(buffer) {
  const sample =
    buffer.subarray(
      0,
      Math.min(buffer.length, 8192)
    );

  for (const byte of sample) {
    if (byte === 0) {
      return true;
    }
  }

  return false;
}

function readDirectory(directory) {
  const entries =
    fs.readdirSync(directory, {
      withFileTypes: true
    });

  entries.sort((a, b) =>
    a.name.localeCompare(
      b.name,
      undefined,
      {
        numeric: true,
        sensitivity: "base"
      }
    )
  );

  const results = [];

  for (const entry of entries) {
    const fullPath =
      path.join(
        directory,
        entry.name
      );

    if (entry.isDirectory()) {
      if (
        shouldExcludeDirectory(
          entry.name
        )
      ) {
        continue;
      }

      results.push(
        ...readDirectory(fullPath)
      );

      continue;
    }

    if (!entry.isFile()) {
      continue;
    }

    if (
      shouldExcludeFile(
        entry.name
      )
    ) {
      continue;
    }

    results.push(fullPath);
  }

  return results;
}

function buildTree(files) {
  const root = {
    type: "directory",
    name: "",
    children: new Map()
  };

  for (const filePath of files) {
    const relative =
      relativePath(filePath);

    const parts =
      relative.split("/");

    let current = root;

    for (
      let i = 0;
      i < parts.length;
      i++
    ) {
      const part = parts[i];

      const isFile =
        i === parts.length - 1;

      if (
        !current.children.has(part)
      ) {
        current.children.set(
          part,
          isFile
            ? {
                type: "file",
                name: part
              }
            : {
                type: "directory",
                name: part,
                children: new Map()
              }
        );
      }

      current =
        current.children.get(part);
    }
  }

  return root;
}

function renderTree(node, prefix = "") {
  const entries =
    [...node.children.entries()]
      .sort(([nameA, valueA], [nameB, valueB]) => {
        if (
          valueA.type !== valueB.type
        ) {
          return valueA.type ===
            "directory"
            ? -1
            : 1;
        }

        return nameA.localeCompare(
          nameB,
          undefined,
          {
            numeric: true,
            sensitivity: "base"
          }
        );
      });

  const lines = [];

  entries.forEach(
    ([name, child], index) => {
      const last =
        index === entries.length - 1;

      const connector =
        last ? "└── " : "├── ";

      lines.push(
        `${prefix}${connector}${name}`
      );

      if (
        child.type === "directory"
      ) {
        const nextPrefix =
          prefix +
          (last ? "    " : "│   ");

        lines.push(
          renderTree(
            child,
            nextPrefix
          )
        );
      }
    }
  );

  return lines
    .filter(Boolean)
    .join("\n");
}

function readTextFile(filePath) {
  const stat =
    fs.statSync(filePath);

  if (
    stat.size >
    MAX_FILE_SIZE
  ) {
    return {
      status: "too-large",
      content:
        `[SKIPPED: file is ${stat.size} bytes, ` +
        `larger than the ${MAX_FILE_SIZE} byte limit]`
    };
  }

  if (
    isBinaryExtension(filePath)
  ) {
    return {
      status: "binary",
      content:
        "[SKIPPED: binary file]"
    };
  }

  const buffer =
    fs.readFileSync(filePath);

  if (isProbablyBinary(buffer)) {
    return {
      status: "binary",
      content:
        "[SKIPPED: binary file]"
    };
  }

  return {
    status: "text",
    content:
      buffer.toString("utf8")
  };
}

function addLineNumbers(content) {
  const lines =
    content.split(/\r?\n/);

  const width =
    String(lines.length).length;

  return lines
    .map((line, index) => {
      const number =
        String(index + 1)
          .padStart(width, " ");

      return `${number} | ${line}`;
    })
    .join("\n");
}

function createDump(files) {
  const tree =
    buildTree(files);

  const output = [];

  output.push(
    "# PROJECT DUMP"
  );

  output.push("");

  output.push(
    `Generated: ${new Date().toISOString()}`
  );

  output.push(
    `Root: ${ROOT}`
  );

  output.push(
    `Files included: ${files.length}`
  );

  output.push("");

  output.push(
    "============================================================"
  );

  output.push(
    "PROJECT STRUCTURE"
  );

  output.push(
    "============================================================"
  );

  output.push("");

  output.push(
    path.basename(ROOT) + "/"
  );

  output.push(
    renderTree(tree)
  );

  output.push("");

  output.push(
    "============================================================"
  );

  output.push(
    "FILE CONTENTS"
  );

  output.push(
    "============================================================"
  );

  output.push("");

  for (const filePath of files) {
    const relative =
      relativePath(filePath);

    const result =
      readTextFile(filePath);

    output.push(
      "============================================================"
    );

    output.push(
      `FILE: ${relative}`
    );

    output.push(
      `STATUS: ${result.status}`
    );

    output.push(
      "============================================================"
    );

    output.push("");

    if (
      result.status === "text"
    ) {
      output.push(
        addLineNumbers(
          result.content
        )
      );
    } else {
      output.push(
        result.content
      );
    }

    output.push("");
  }

  return output.join("\n");
}

function main() {
  console.log(
    "[dump] Project root:",
    ROOT
  );

  console.log(
    "[dump] Scanning project..."
  );

  const files =
    readDirectory(ROOT);

  console.log(
    `[dump] Found ${files.length} files.`
  );

  console.log(
    "[dump] Building project dump..."
  );

  const dump =
    createDump(files);

  fs.writeFileSync(
    OUTPUT,
    dump,
    "utf8"
  );

  const size =
    fs.statSync(OUTPUT).size;

  console.log(
    "[dump] Created:",
    OUTPUT
  );

  console.log(
    `[dump] Size: ${size} bytes`
  );

  console.log(
    "[dump] Done."
  );
}

main();

Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.