mavtools
← Catalog
CLIv1.0.0· by Motion Mavericks

bundle

Pack a set of files into one Markdown document ready to paste into an AI chat.

universal#cli#context#workflow
command.md · markdown
---
name: bundle
description: Pack a set of files into one Markdown document ready to paste into an AI chat.
platforms: [universal]
tags: [cli, context, workflow]
version: 1.0.0
author: Motion Mavericks
license: MIT
---

# bundle

A zero-dependency Node script that concatenates the files you name into a single
fenced Markdown document — the fastest way to hand an AI a coherent slice of a
codebase without copy-pasting file by file.

## Usage

```bash
node bin/bundle.mjs src/index.ts src/util.ts > context.md
```

Each file is emitted with a heading and a language-tagged code fence. Paths that
do not exist are reported to stderr and skipped, so a bad glob never corrupts the
output. Exits non-zero if no file could be read. Files containing backtick fences
are wrapped in a longer fence automatically, so bundling Markdown stays valid.

## Install

Copy `bin/bundle.mjs` anywhere on your `PATH` (or into a project's `scripts/`).
Requires Node 18+.
bin/bundle.mjs · js
#!/usr/bin/env node
// bundle — pack named files into one Markdown document for pasting into an AI chat.
// Usage: node bundle.mjs <file> [<file> ...]  >  context.md
import { readFile } from "node:fs/promises";
import { extname, relative } from "node:path";

const EXT_LANG = {
  ".ts": "ts", ".tsx": "tsx", ".js": "js", ".jsx": "jsx", ".mjs": "js",
  ".py": "python", ".rb": "ruby", ".go": "go", ".rs": "rust", ".java": "java",
  ".json": "json", ".yaml": "yaml", ".yml": "yaml", ".md": "markdown",
  ".sh": "bash", ".css": "css", ".html": "html", ".sql": "sql",
};

const files = process.argv.slice(2);
if (files.length === 0) {
  console.error("usage: bundle <file> [<file> ...]");
  process.exit(1);
}

const out = [];
for (const file of files) {
  try {
    const body = await readFile(file, "utf8");
    const lang = EXT_LANG[extname(file)] ?? "";
    // Fence one backtick longer than the longest run in the body, so files that
    // themselves contain ``` (every Markdown code block) don't break out.
    const runs = body.match(/`+/g) ?? [];
    const fence = "`".repeat(Math.max(3, ...runs.map((r) => r.length + 1)));
    out.push(`## ${relative(process.cwd(), file)}\n\n${fence}${lang}\n${body.replace(/\s+$/, "")}\n${fence}\n`);
  } catch (err) {
    console.error(`skipped ${file}: ${err.code === "ENOENT" ? "not found" : err.message}`);
  }
}

if (out.length === 0) {
  console.error("bundle: no files bundled");
  process.exit(1);
}

process.stdout.write(out.join("\n"));