This repository follows the Conventional Commits specification for commit messages. This means that each commit message should be structured in the following format:
[optional body]
[optional footer(s)]
Where:
<type>is a required field that indicates the type of change being made. Common types includefeatfor new features,fixfor bug fixes,docsfor documentation changes,stylefor code formatting,refactorfor code changes that neither fix a bug nor add a feature, andtestfor adding or modifying tests.[optional scope]is an optional field that provides additional context about the change, such as the area of the codebase affected (e.g.,api,ui,database).<description>is a required field that provides a brief summary of the change.[optional body]is an optional field that can include a more detailed description of the change, including the motivation for the change and any relevant background information.[optional footer(s)]is an optional field that can include any additional information, such as breaking changes or issues closed by the commit.
Examples of valid commit messages:
fix(ui): resolve issue with button alignment
docs: update README with installation instructions
style: reformat code using Prettier
refactor: simplify data fetching logic
test: add unit tests for user model
By following these conventions, we can maintain a clear and consistent commit history that makes it easier to understand the changes being made and the reasons behind them. This also helps with generating changelogs and automating releases based on commit messages.
Please do not co-author commits with AI assistants, as this can create confusion about the source of the changes and may not accurately reflect the contributions of human developers. Instead, focus on writing clear and descriptive commit messages that accurately convey the intent and impact of the changes being made.
See conventions/ for the full conventions with examples in both TypeScript and Go:
conventions/QUALITY.md- API design: verb+noun entry points, category objects, single call backbone, no global state, fail-early errors.conventions/PERFORMANCE.md- Performance: data structure selection, bounded collections, early exits, signal over polling, hot-path allocations, batching, coordination.conventions/DOCUMENTATION.md- Writing docs: lead with the answer, active voice, "must" vs "we recommend", show the artifact (code, tree, config) instead of describing it, rarely use em-dashes, sentence-case headings with intro paragraphs, descriptive links, no duplicated facts.
If the contents of any of those three files are not visible in your current context, read the file before doing any task that touches code or prose. They are the source of truth; do not guess.
Default to no comments. The codebase is for professional developers; the code is the source of truth.
Add a comment ONLY for:
- Constraints: hidden environmental requirements (
Bun's $bunfs is read-only at boot, so factories must defer I/O). - Workarounds: "this looks wrong but isn't" notes (
AbortError name varies across platforms, hence the dual check). - Invariants: subtle properties of the data the code relies on (
transitives is omitted, never []; keeps diffs clean). - Surprises: behaviour a reader would otherwise misread (
Java's File accepts forward slashes on Windows).
Do NOT add a comment for:
- Reasoning narratives about why a refactor was done. That belongs in the commit message or PR description.
- Multi-paragraph file headers that list what the exports do. Each export's own JSDoc covers that.
- Inline justifications of ordinary code shape (
we deliberately use a Map here because…). If it's the obvious shape, don't justify it. - Restating what the next line does. Identifiers are self-documenting.
When in doubt: delete the comment. If a future reader is confused without it, they can git blame to the commit message.
Vite+ (vp) drives the development loop. Bun produces the shipped CLI binary. See the Vite+ block below for the full command surface.
vp install- install dependenciesvp check- format, lint, and type checks (Oxlint + Oxfmt + tsgo)vp test- run tests via the bundled Vitestvp dev/vp pack- library build during developmentbun build --compile --outfile=bin/pluggy ./src/index.ts- standalone CLI binary for releases
Vite+'s pack only emits JavaScript. The standalone executable is always produced with Bun's --compile flag, which is what ships to users via the install scripts.
Use vite-plus/test (the Vitest wrapper). Do not install vitest directly.
import { expect, test } from "vite-plus/test";
import { getPlatform } from "../src/platform/index.ts";
test("spigot platform is registered", () => {
expect(getPlatform("spigot").id).toBe("spigot");
});Tests live next to the code they cover as *.test.ts. Network-dependent tests (platform download, getVersions) hit real upstream APIs intentionally — do not mock them.
playground/ at the repo root is gitignored and exists for ad-hoc manual testing of the CLI — building the binary with bun build --compile --outfile=bin/pluggy ./src/index.ts, then running pluggy init, pluggy build, pluggy dev, etc. against a scratch project to verify the actual user experience. Use it whenever a change has a UX surface that the test suite can't cover (interactive prompts, error formatting, generated project.json shape, BuildTools output, dev server startup). Create subdirectories per scenario (playground/spigot-1.21/, playground/cross-family/, …) and feel free to leave them around — nothing in playground/ is committed.
When you spot a bug that isn't in scope for the current task — typically while exercising the CLI in playground/ or while reviewing unrelated code — log it as a GitHub issue so it doesn't get forgotten. The workflow:
- Search first:
gh issue list --search "<keywords>" --state all. Try a couple of phrasings (the symptom, the affected command, the affected file) before assuming it's new. - If no match, surface it to the human: describe the bug, the reproduction, and a draft body, then ask whether to file. Don't open issues unprompted.
- On approval, file with
gh issue create --title "…" --label bug --body "…". Match the bug-report template's sections (Summary / Reproduction / Expected / Actual / pluggy version /pluggy doctoroutput / Additional context) —gh issue createdoesn't auto-apply YAML form templates, so reproduce the structure in the body. Templates live in.github/ISSUE_TEMPLATE/. - For a bug found while finishing a PR, link the issue from the PR description so the connection is visible without searching.
- Every command lives in
src/commands/<name>.tsand exports a factoryxxxCommand()that returns aCommand(fromcommander).src/index.tsimports the factories and callsprogram.addCommand()— keepindex.tsthin. - Inside an action, read global flags with
this.optsWithGlobals()(the action must be a non-arrowfunctionsothisbinds). Never reference a module-levelcurrentProject— resolve fresh inside the action. - Every command must honour the global
--jsonflag: emit a single structured JSON object on success, and a{ status: "error", message, exitCode }object on failure. Never mix JSON and human text in the same output. - Throw
InvalidArgumentError(fromcommander) for user-input problems; throw regularErrorfor runtime/IO failures. Both are caught by the top-level handler insrc/index.ts, which formats them per--json. - Use
@inquirer/promptsfor interactive prompts.--yesor--jsonmust bypass prompts entirely — with--json, prompts become errors rather than hangs. - New platform providers go through
createPlatform((ctx) => ({ ... }))and must be imported fromsrc/platform/index.tsfor the side-effect registration.createPlatformmust not perform I/O at module-load time — defer disk writes to the command that needs them (otherwise the Bun-compiled binary crashes reading from the read-only$bunfspath).
Many modules are stubs: their functions throw new Error("not implemented: <name>"). When implementing a stub:
- Write or un-skip the contract tests in
<module>.test.tsfirst. They'redescribe.skipblocks with concrete assertions — they define the contract the implementation must satisfy. - Replace the
throwbody with the implementation. - Remove the
.skipfrom the tests and confirm they pass withvp test <module>. - Do not change exported function signatures, argument shapes, or return types without checking every caller in the repo — these are the contract other modules rely on.
This pattern lets parallel agents implement different modules without blocking on each other.
Every file path, process spawn, signal, and UI concern must work identically on macOS, Linux, and Windows:
- Paths in
project.json/pluggy.lockare always forward-slashed (normalize viaportable.toPosixPath). - Link large files with
portable.linkOrCopy(hardlink first, copy fallback — never symlink). - Signal handling goes through
portable.installShutdownHandlerwhich wrapschild.kill()(the cross-platform Node shim). - Write generated files with LF line endings (
portable.writeFileLF). - Never spawn a shell — always call
spawn(cmd, args, ...)directly. Node handles.exeon Windows.
This project is using Vite+, a unified toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Vite+ wraps runtime management, package management, and frontend tooling in a single global CLI called vp. Vite+ is distinct from Vite, but it invokes Vite through vp dev and vp build.
vp is a global binary that handles the full development lifecycle. Run vp help to print a list of commands and vp <command> --help for information about a specific command.
- create - Create a new project from a template
- migrate - Migrate an existing project to Vite+
- config - Configure hooks and agent integration
- staged - Run linters on staged files
- install (
i) - Install dependencies - env - Manage Node.js versions
- dev - Run the development server
- check - Run format, lint, and TypeScript type checks
- lint - Lint code
- fmt - Format code
- test - Run tests
- run - Run monorepo tasks
- exec - Execute a command from local
node_modules/.bin - dlx - Execute a package binary without installing it as a dependency
- cache - Manage the task cache
- build - Build for production
- pack - Build libraries
- preview - Preview production build
Vite+ automatically detects and wraps the underlying package manager such as pnpm, npm, or Yarn through the packageManager field in package.json or package manager-specific lockfiles.
- add - Add packages to dependencies
- remove (
rm,un,uninstall) - Remove packages from dependencies - update (
up) - Update packages to latest versions - dedupe - Deduplicate dependencies
- outdated - Check for outdated packages
- list (
ls) - List installed packages - why (
explain) - Show why a package is installed - info (
view,show) - View package information from the registry - link (
ln) / unlink - Manage local package links - pm - Forward a command to the package manager
- upgrade - Update
vpitself to the latest version
These commands map to their corresponding tools. For example, vp dev --port 3000 runs Vite's dev server and works the same as Vite. vp test runs JavaScript tests through the bundled Vitest. The version of all tools can be checked using vp --version. This is useful when researching documentation, features, and bugs.
- Using the package manager directly: Do not use pnpm, npm, or Yarn directly. Vite+ can handle all package manager operations. For Bun-specific operations that Vite+ does not wrap (notably
bun build --compile), callingbundirectly is expected. - Always use Vite commands to run tools: Don't attempt to run
vp vitestorvp oxlint. They do not exist. Usevp testandvp lintinstead. - Running scripts: Vite+ built-in commands (
vp dev,vp build,vp test, etc.) always run the Vite+ built-in tool, not anypackage.jsonscript of the same name. To run a custom script that shares a name with a built-in command, usevp run <script>. - Do not install Vitest, Oxlint, Oxfmt, or tsdown directly: Vite+ wraps these tools. They must not be installed directly. You cannot upgrade these tools by installing their latest versions. Always use Vite+ commands.
- Use Vite+ wrappers for one-off binaries: Use
vp dlxinstead of package-manager-specificdlx/npxcommands. - Import JavaScript modules from
vite-plus: Instead of importing fromviteorvitest, all modules should be imported from the project'svite-plusdependency. For example,import { defineConfig } from 'vite-plus';orimport { expect, test, vi } from 'vite-plus/test';. You must not installvitestto import test utilities. - Type-Aware Linting: There is no need to install
oxlint-tsgolint,vp lint --type-awareworks out of the box.
For GitHub Actions, consider using voidzero-dev/setup-vp to replace separate actions/setup-node, package-manager setup, cache, and install steps with a single action.
- uses: voidzero-dev/setup-vp@v1
with:
cache: true
- run: vp check
- run: vp test- Run
vp installafter pulling remote changes and before getting started. - Run
vp configonce per fresh clone to install the committed.vite-hooks/(pre-commit runsvp staged; pre-push runsvp check && vp test). SetVITE_GIT_HOOKS=0to skip hooks for an emergency push. - Run
vp checkandvp testbefore every push, including docs-only changes. Oxfmt formats markdown too, so a missedvp checkon a.mdfile fails CI just like a code change. Usevp check --fixto auto-fix formatting, then re-runvp checkto confirm clean. - For release-shaped changes, verify
bun build --compile --outfile=bin/pluggy ./src/index.tsstill produces a working binary.