repo2md: Shipping My First VS Code Extension
A friend had a simple, real problem: he wanted one Markdown file that always mirrors his project (folder tree plus every file's contents) so he can paste full context into Copilot or ChatGPT in one go. He works in Playwright tests, and hand-assembling that context every time was...
A favour for a friend that ended up on the Marketplace
A friend had a simple, real problem: he wanted one Markdown file that always mirrors his project (folder tree plus every file's contents) so he can paste full context into Copilot or ChatGPT in one go. He works in Playwright tests, and hand-assembling that context every time was eating his day.
That favour became repo2md, my first published VS Code extension. This post is the honest build log: the surprisingly small core, the publishing hoops nobody warns you about, and the part that actually taught me the most, which was watching a real user hit real edges and redesigning around them.
The build: plain JavaScript, zero dependencies, one core rule
The first working version took about ten minutes, and that is not a brag about
speed, it is a compliment to the VS Code extension API. An extension is a
Node.js module with an activate function; VS Code itself provides the entire
API at runtime. My package.json has zero dependencies and no build step.
Plain JS, two files: the VS Code wiring, and a generator library that walks the
tree and emits fenced code blocks. The generator is deliberately separate so a
tiny CLI harness can test it without launching VS Code at all.
The one rule I set before writing anything: the tool must be safe to point at a real repo. A context file that faithfully embeds your secrets is a disaster with good intentions, and this class of tool has exactly that risk. So the generator never embeds:
.env*and other secret-shaped files- lock files and binaries (null-byte sniff plus extension check)
- anything over 64 KB
- its own output file (the loop guard: a generator that includes its previous output grows without bound)
And because my friend's world is Playwright, a profile system skips report folders, browser user-data directories, and storage-state files, which contain session cookies, meaning auth secrets. The scariest find of the whole project was in his real repo: a full browser profile, cookies included, committed to git. The tool now refuses that category by design, and reports anything it withheld in the output header rather than skipping silently.
Publishing: the hoops, recorded for the next person
The Marketplace steps that cost me real time, in one place:
- The publisher ID is case-sensitive everywhere. The
"publisher"field in package.json must match your Marketplace publisher ID exactly. - The icon must be PNG. SVG is rejected outright.
- You do not need a token to publish. Every guide walks you through Azure
DevOps personal access tokens for the
vsceCLI. The web upload path (Marketplace management page, New extension, upload the.vsix) needs no PAT at all. Package withnpx @vscode/vsce packageand drag the file in. - Every upload must have a strictly higher version. The Marketplace rejects re-uploading a version number that ever existed, even a broken one. You learn this exactly one way.
.vscodeignorematters. My first package quietly bundled a test fixture playground. The final.vsixis seven files, 26 KB.
The real lesson: v0.1 to v0.4 in the hands of one honest user
The favour version dumped the whole repo into the context file. My friend's first feedback: "too many files." He was right, and each iteration after that came from him using it and hitting a real edge:

Three of those steps deserve a sentence each:
Blank-first was a design reversal, not a feature. The tool's default philosophy flipped from "everything except what you exclude" to "nothing except what you include". For a context tool feeding an AI, opt-in is the right polarity: you curate what the model sees. The whole-repo dump survives only as an explicit escape-hatch command.
The status-bar menu exists because my friend uses Windows and a mouse. I had bound everything to the command palette. Watching a real user fumble where you assumed muscle memory is worth a hundred design reviews.
Folder picks are live, so exclusion had to exist. Picking a folder
includes files created in it later (that is the point), but then you need to
subtract one noisy child without giving up the folder. Add and Remove now
compose properly: pick src, exclude src/utils/math.js, keep everything
else, including files that do not exist yet.
Selections persist in .vscode/settings.json, deliberately: it is visible,
it is committable, and VS Code watches it for changes for free. Boring
storage in the place users already understand beats a clever custom format.
Takeaways
- A VS Code extension is a smaller project than you think. Plain JS, zero deps, no build step is a fully valid shape. The API does the heavy lifting.
- If your tool exports file contents, safety rails are a launch feature, not polish. Refuse secrets by category, and say what you withheld.
- Publishing friction is all trivia (case-sensitive IDs, PNG icons, monotonic versions). Now you have the list.
- One honest user beats a roadmap. Every meaningful design decision in this tool came from watching a real person hit a real edge and asking for the honest fix instead of the quick one.
Pictures to add later
- The Marketplace listing page (the milestone shot)
- The extension in action: Explorer right-click menu with Add to Context, and the status-bar menu open
- A generated PROJECT_CONTEXT.md open in the editor, tree + fenced contents visible