Program note · Writing · 13 Aug 2026
I turned my project scaffolder into a protocol
Packkit started as a tool that wrote a modern JavaScript package for me. The useful part is what it became, a small protocol that lets any language plug in and every surface drive it the same way.

For years my ritual for a new project was to copy the last repo, delete the parts that did not apply, fix the tsconfig, then re-add the CI file I always forget. Every new package started as an archaeology dig through an old one. The tools that promised to fix this each had an opinion I did not share, or they scaffolded one framework and nothing else, or they stopped caring the moment the files were on disk.
So I built create-packkit: a generator that writes a modern JS or TS package, with the linter, the test runner, the build, the CI, and the community files already wired together. That part is ordinary. The part worth writing about is what it turned into. Underneath the JavaScript tool now sits @packkit/core, a small protocol that says what a generator is, so a Python or Go generator can plug in and every surface can drive all of them the same way.
The one rule: core never learns a language
The whole design lives on a single constraint. @packkit/core does not know what npm is. It has never heard of pyproject.toml or go.mod. It cannot, because the moment core knows one language's packaging, it starts to model that language, and the next language has to fight it.
A generator implements a contract instead. It declares an id and a language, lists its presets, hands back an option schema, and has a createProject that returns files. Optionally it has an upgradeProject for later. Core holds the shape of that contract and nothing about the language behind it.
The packaging differences do exist, they just live in the generator, behind a seam called a ManifestDiffer. The JavaScript generator has one that understands package.json. The Python generator has one that understands pyproject.toml. The Go generator has one that parses go.mod line by line. Core sees a manifest diff, never the file format that produced it.
The output is a contract, not a language
Files are only half of what a generator returns. The other half is one language-neutral thing: a deployment contract. It is a small tagged object, static, service, worker, library, or cli, that says what kind of thing was built and how to run it. A React app returns { type: 'static', buildCommand, outputDirectory }. A Python HTTP service and a Node HTTP service and a Go HTTP service all return the same service contract, with a different runtime string.
This matters later. A deploy provider reads that contract and decides whether it can host the thing. It never asks what language wrote it. That is a whole post on its own, so I will leave it there for now.
What "Packkit compatible" actually means
A protocol is only as real as its test. Core ships runGeneratorConformanceSuite, an executable definition of a valid generator. It is not documentation about the contract, it is the contract, as assertions. A generator either passes the suite or it is not a Packkit generator. When I add a language, that suite is the first thing I make go green, before any of the fun.
Here is the shape as it stands today:
@packkit/core the protocol + conformance suites
create-packkit JavaScript / TypeScript generator
create-packkit-py Python generator
create-packkit-go Go generator
packkit-mcp an MCP server, so an agent scaffolds as a native tool
packkit-web a browser configurator, no install
provider-netlify reads a deployment contract, deploys it
provider-aws same idea, emits TerraformThree languages, one protocol version, several surfaces. Every generator produces library, CLI, service, and worker targets, plus the JavaScript one adds framework apps. A host integrates once, against core, and drives any of them by id.
What is still rough
Two honest wrinkles. First, there is a deliberate version split: the generators pin an older core minor as a dependency, while providers accept a wider range. It is invisible to a user running npx create-packkit, but it means "what version is the ecosystem on" has more than one answer, and I had to build a checker so the answer never drifts into a real conflict.
Second, and more fundamental: a protocol with one implementation is just a class with extra ceremony. The claim that core does not model npm is only believable once a second, very different language plugs in and forces nothing to change. That is the next post. I wrote the Python and Go generators partly to use them, and partly to find out whether the line I drew through core was real or wishful.
If you want to poke at the thing itself, the configurator runs in the browser at packkit-web.pages.dev, no install, and the code lives under the PackkitLabs org.