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.toml files are project-local.
  • Don’t add a patcher when a template overlay would suffice.

project-kit · MIT License · GitHub

This site uses Just the Docs, a documentation theme for Jekyll.