Adding a New Project Type
A project type is the combination of a manifest, templates, and (optionally) patchers. The core engine doesn’t change.
1. Write the manifest
Create manifests/<id>.yml. Start with a minimal shape:
version: 1
id: my-type
language: ruby
type: application
capabilities:
supported: [coverage, github-actions, mise]
default: [mise, github-actions]
variables:
ruby_version:
default: "3.4"
templates:
- shared
- ruby
- my-type
mise:
tools:
ruby: ""
tasks:
test:
run: "bundle exec rspec"
Run bundle exec exe/project-kit list to verify it loads, or bundle exec rspec spec/project_kit/manifest_spec.rb to confirm the schema validator accepts it.
2. Add templates
templates/
my-type/
bin/
__name__.erb # filename token expands to project name
lib/
__name__.rb.erb # ERB rendered with project context
If your type needs language-baseline files (e.g. a .gitignore for Node), add them under templates/<language>/ instead of templates/my-type/.
If the type wraps an existing ecosystem generator (bundle gem, rails new, pnpm create, …), declare an initializer block in the manifest. Avoid duplicating files the generator already produces — let the overlay system replace what you want differently.
3. (Optional) Add a patcher
Imperative cleanup goes in a Ruby class under lib/project_kit/patchers/<ecosystem>/<patcher>.rb:
require "project_kit/patchers"
module ProjectKit
module Patchers
module MyEcosystem
class FixThing < Base
def call
edit("config/whatever.yml") do |content|
content.sub("foo", "bar")
end
end
end
end
end
end
Reference it in the manifest:
patchers:
- my_ecosystem/fix_thing
4. Add a snapshot test
Drop a spec under spec/integration/ that uses generate_project from snapshot_helper.rb. For types with an upstream initializer, pass a fake_initializer: lambda that mimics the tool’s output — your test runs end-to-end without shelling out to a real toolchain.
RSpec.describe "my-type project type", :integration do
it "generates the expected tree" do
with_tmpdir do |root|
dest = generate_project(type: "my-type", name: "demo", dest_root: root)
expect(file_tree(dest)).to include("mise.toml", "lib/demo.rb")
end
end
end
What you should not do
- Don’t encode conditional logic in YAML manifests. Add a patcher.
- Don’t duplicate baseline files (README, LICENSE, .editorconfig, etc.) per project type — they live in
templates/shared/. - Don’t couple to a global mise config. Generated
mise.tomlfiles are project-local. - Don’t add a patcher when a template overlay would suffice.