Project Detection
How vibe init detects your project and how to write custom language detectors and addon detectors.
Overview
vibe init automatically detects your project’s language, framework, and infrastructure — then generates a Vibefile with appropriate targets. No LLM call required.
Detection is built on two pluggable interfaces: language detectors identify the primary project type, and addon detectors find tools and platforms that contribute additional targets. Both use a registry pattern — detectors register themselves at import time and are run automatically.
This system is designed to be extended. You can contribute new detectors for additional languages, or write addon detectors for tools and platforms you use.
How detection works
When you run vibe init, the CLI follows this flow:
vibe init
1. Try language detection at the repo root
→ If a language matches → single-project mode
2. If no match at root, scan immediate subdirectories
→ If sub-projects found → monorepo mode
3. Run addon detectors (Docker, Helm, Makefile, etc.)
4. Merge all targets into one Vibefile
5. Write Vibefile
Single-project mode
When a language detector matches at the repo root (e.g. finds go.mod), the CLI generates targets for that language and runs all addon detectors at the root:
$ vibe init
✓ Go project detected (go 1.25.0, module: github.com/myorg/myapp)
✓ Docker detected → docker
✓ Vibefile created with 9 targets
Monorepo mode
When no language matches at the root, the CLI scans immediate subdirectories. Each detected sub-project gets its targets prefixed with the directory name:
$ vibe init
✓ Monorepo detected — 2 sub-projects found
✓ go/ — Go project (go 1.25.7)
✓ go/Docker detected → go-docker
✓ ui/ — Nextjs project (nextjs 15.1.0)
✓ Makefile detected → make-build, make-test, make-lint
✓ Vibefile created with 24 targets
Targets become go-build, go-test, ui-dev, ui-lint, etc. Recipes include directory context (e.g. “in the go/ directory, compile the Go binary…”). Addons are also scanned per-subdirectory — a Dockerfile inside go/ produces go-docker.
Directories like .git, node_modules, vendor, dist, and hidden directories are skipped during scanning.
Built-in language detectors
| Language | Detects | Targets generated |
|---|---|---|
| Go | go.mod, go.work |
build, test, lint, fmt, vet, check, clean, install |
| Next.js | package.json with next dependency |
dev, build, start, lint, typecheck, test, e2e, check, clean, deps |
The Go detector also handles workspace projects (go.work) — it parses all use directives, collects module paths, and generates recipes that reference all workspace modules.
The Next.js detector identifies the package manager (npm, pnpm, yarn, bun), TypeScript support, router type (App/Pages), test frameworks (vitest, jest, playwright, cypress), and Prettier.
Built-in addon detectors
| Addon | Detects | Targets contributed |
|---|---|---|
| Docker | Dockerfile |
docker |
| Fly.io | fly.toml |
deploy |
| Vercel | vercel.json |
deploy |
| Cloudflare | wrangler.toml or wrangler.json |
cf-dev, cf-deploy |
| Helm | Chart.yaml (root or subdirectories) |
helm-lint, helm-template, helm-package |
| Makefile | Makefile, GNUmakefile |
make-<target> for each discovered target |
The Makefile addon parses existing Makefile targets and wraps them — preferring .PHONY targets when available, and skipping internal targets (all, default, _-prefixed).
Writing a language detector
A language detector identifies a project’s primary language and tooling. It implements the Detector interface:
package detect
type Detector interface {
Name() string
Detect(repoRoot string) (*ProjectInfo, bool)
}
Name() returns the language identifier (e.g. "go", "python"). Detect() checks the given directory for language-specific files and returns a ProjectInfo with project metadata, or false if the language isn’t detected.
ProjectInfo
type ProjectInfo struct {
Language string // "go", "node", "python", "rust"
Framework string // "gin", "next", "flask" (optional)
PackageManager string // "go", "npm", "pip", "cargo"
Version string // language version from go.mod, .nvmrc, etc.
BinaryName string // inferred from module path or package name
Module string // go module path, npm package name, etc.
Modules []string // workspace module directories (go.work)
HasTests bool // test files detected
Metadata map[string]string // detector-specific extras
}
Example: a minimal Python detector
package python
import (
"path/filepath"
"github.com/vibefile-dev/vibe/detect"
)
func init() { detect.Register(&Detector{}) }
type Detector struct{}
func (d *Detector) Name() string { return "python" }
func (d *Detector) Detect(repoRoot string) (*detect.ProjectInfo, bool) {
// Check for pyproject.toml, setup.py, or requirements.txt
for _, file := range []string{"pyproject.toml", "setup.py", "requirements.txt"} {
if detect.FileExists(filepath.Join(repoRoot, file)) {
return &detect.ProjectInfo{
Language: "python",
PackageManager: "pip",
BinaryName: filepath.Base(repoRoot),
Metadata: make(map[string]string),
}, true
}
}
return nil, false
}
The key points:
- Package name — put the detector in its own sub-package under
detect/(e.g.detect/python) init()function — calldetect.Register(&Detector{})to register at import timedetect.FileExists()— helper for checking file presence- Return
falsewhen no match — the registry moves on to the next detector
Pairing with a template provider
A detector identifies the project. A template provider generates the actual Vibefile targets. Implement TemplateProvider:
package detect
type TemplateProvider interface {
Language() string
Provide(project *ProjectInfo) *Template
}
Register it in init() with detect.RegisterTemplate(&TemplateProvider{}). The template provider receives the ProjectInfo populated by the detector and returns a Template with variables and targets.
Example: Python template provider
package python
import "github.com/vibefile-dev/vibe/detect"
func init() {
detect.Register(&Detector{})
detect.RegisterTemplate(&TemplateProvider{})
}
type TemplateProvider struct{}
func (p *TemplateProvider) Language() string { return "python" }
func (p *TemplateProvider) Provide(project *detect.ProjectInfo) *detect.Template {
return &detect.Template{
Variables: []detect.TemplateVariable{
{Key: "model", Value: "claude-sonnet-4-6"},
{Key: "name", Value: project.BinaryName},
},
Targets: []detect.TemplateTarget{
{
Name: "test",
Section: "build & test",
Recipe: "run all Python tests with pytest and verbose output",
},
{
Name: "lint",
Section: "build & test",
Recipe: "run ruff or flake8 linting on the entire project",
},
{
Name: "fmt",
Section: "build & test",
Recipe: "format all Python files using ruff format or black",
},
{
Name: "check",
Section: "quality gates",
Dependencies: []string{"fmt", "lint", "test"},
Recipe: "print a summary of what passed — all quality gates complete",
},
},
}
}
Wiring it in
For the detector to run, it must be imported in cmd/init.go with a blank import:
import (
_ "github.com/vibefile-dev/vibe/detect/python"
)
This triggers the init() function which registers both the detector and template provider.
Writing an addon detector
Addon detectors find tools, platforms, and infrastructure and contribute additional targets to the Vibefile. They implement the Addon interface:
package detect
type Addon interface {
Name() string
Detect(repoRoot string) *AddonResult // nil if not detected
}
Detect() returns an AddonResult with a human-readable label and a list of targets, or nil if the tool isn’t detected.
AddonResult
type AddonResult struct {
Label string // human-readable label, e.g. "Docker", "Helm"
Targets []TemplateTarget // targets contributed by this addon
}
Example: a Terraform addon
package terraform
import (
"path/filepath"
"github.com/vibefile-dev/vibe/detect"
)
func init() { detect.RegisterAddon(&Addon{}) }
type Addon struct{}
func (a *Addon) Name() string { return "terraform" }
func (a *Addon) Detect(repoRoot string) *detect.AddonResult {
// Look for .tf files or terraform directory
if !detect.FileExists(filepath.Join(repoRoot, "main.tf")) {
return nil
}
return &detect.AddonResult{
Label: "Terraform",
Targets: []detect.TemplateTarget{
{
Name: "tf-init",
Section: "infrastructure",
Recipe: "run terraform init to initialize the working directory",
},
{
Name: "tf-plan",
Section: "infrastructure",
Recipe: "run terraform plan and show the execution plan",
},
{
Name: "tf-apply",
Section: "infrastructure",
Dependencies: []string{"tf-plan"},
Recipe: "run terraform apply to apply the planned changes",
},
},
}
}
The same registration pattern applies — blank import in cmd/init.go:
import (
_ "github.com/vibefile-dev/vibe/detect/terraform"
)
How addons merge
When both a language template and addons contribute targets, they’re merged into a single Vibefile. If a target name conflicts (e.g. both the language template and an addon generate a deploy target), the first writer wins and the conflict is logged in debug output.
YAML template overrides
You don’t need to write Go code to customize the generated Vibefile. Templates can be overridden with YAML files:
.vibe/templates/<lang>.yaml— project-local override~/.vibe/templates/<lang>.yaml— user-global override
These take priority over built-in templates.
Template format
variables:
- key: model
value: claude-sonnet-4-6
- key: name
value: my-project
targets:
- name: build
section: "build & test"
recipe: "compile the project for production"
- name: test
section: "build & test"
recipe: "run all tests with verbose output"
- name: check
section: "quality gates"
dependencies: [test, build]
recipe: "all quality gates passed"
- name: deploy
section: "infrastructure"
recipe: "deploy to production"
directives:
- "@network outbound"
Each target can have:
name— target name (required)recipe— plain-English recipe string (required)section— section header for grouping in the Vibefiledependencies— list of dependency target namesdirectives— list of directive strings (e.g.@network outbound)
Current detector layout
The detection code lives in the detect/ package with each detector in its own sub-package:
detect/
├── detect.go ← Detector and Addon interfaces, registry, scanning
├── project.go ← ProjectInfo, SubProject, AddonResult types
├── template.go ← Template types, resolution, generation
├── helpers.go ← FileExists and shared utilities
├── golang/
│ ├── detect.go ← Go language detector
│ └── template.go ← Go template provider
├── nextjs/
│ ├── detect.go ← Next.js language detector
│ └── template.go ← Next.js template provider
├── docker/
│ └── addon.go ← Docker addon
├── fly/
│ └── addon.go ← Fly.io addon
├── vercel/
│ └── addon.go ← Vercel addon
├── cloudflare/
│ └── addon.go ← Cloudflare addon
├── helm/
│ └── addon.go ← Helm addon
└── makefile/
└── addon.go ← Makefile addon
Each sub-package has an init() function that registers with the central registry. This keeps detectors self-contained and independently maintainable.