Skip to content

verndale/ai-commit

Repository files navigation

@verndale/ai-commit

AI-assisted Conventional Commits with bundled commitlint so generated messages match the same rules enforced in Git hooks.


Requirements

Requirement Notes
Node.js >=24.14.0 (see engines in package.json)
Package manager This repo uses pnpm. Enable with Corepack: corepack enable.

Quick start

Do this from the directory that contains your app’s package.json (in a monorepo that is often not the git repository root).

  1. Add the dependency

    pnpm add -D @verndale/ai-commit

    npm and Yarn work too (npm install -D @verndale/ai-commit). Where this doc says pnpm exec, use npx, yarn exec, or your usual equivalent.

  2. Run init (merges env files, configures Husky when needed, writes hooks, updates package.json when applicable)

    pnpm exec ai-commit init
  3. Install dependencies if init changed package.json or ran Husky for the first time — init prints a line like:

    Next: run \pnpm install` …orNext: run `cd … && pnpm install` …`

    Run that command (it picks pnpm / npm / yarn / bun from the nearest lockfile).

  4. Set your API key in .env and/or .env.local (same directory as that package.json):

    OPENAI_API_KEY=sk-...

    If both files define a key, .env.local wins.


How paths work

Term Meaning
Package root First directory with package.json, walking up from your current directory toward the git root. If none is found, the current working directory is used. Env files and package.json edits use this directory.
Git root git rev-parse --show-toplevel. Husky and hook files live here (or under core.hooksPath).

If package root and git root differ, hook scripts cd into the package root before running ai-commit.


What ai-commit init does (default)

Environment

  • Merges ai-commit-related keys into .env.local if that file exists; otherwise into .env (creates .env from the bundled template if missing). If .env.local exists, .env is not written for this merge.
  • --force never wholesale-replaces .env.local (append / document keys only).
  • Also updates the example env file on disk: prefers .env.local.example when it exists, then .env.example, then .env-example, else creates .env.example. If .env.local.example exists alongside .env.example and/or .env-example, .env.local.example is used and a warning is printed. If both .env.example and .env-example exist (and there is no .env.local.example), .env.example is used and a warning is printed.
  • The npm package still ships the hyphenated template as .env-example.

Husky

  • If the Husky shim .husky/_/h is missing (Husky not installed for this layout), runs npx husky@9 init at the git root. That sets core.hooksPath to .husky/_ and creates the h runner inside .husky/_/.
  • Hooks directory: Git’s core.hooksPath (relative to the git root), or <git-root>/.husky when unset. Invalid or out-of-repo paths fall back to .husky at the git root with a warning.

package.json (at package root)

  • Adds commit, prepare, and devDependencies.husky when missing.

Hook files

  • Husky 9 (core.hooksPath = .husky/_): Git runs short entry scripts under .husky/_/ that only source h. Husky’s h then executes the real scripts at .husky/prepare-commit-msg and .husky/commit-msg (parent folder). Init writes both layers: stubs in _/, and pnpm exec / npx commands in .husky/ (with optional cd into the package root in a monorepo).
  • core.hooksPath = .husky: One file per hook under .husky/ that sources _/h and runs the same commands (no separate stub layer).
  • Removes Husky’s default .husky/pre-commit when it is only npm / pnpm / yarn test (so commits are not blocked by tests). Custom pre-commit files are left alone.
  • Existing hook files are left unchanged unless you run ai-commit init --force. If a previous init put ai-commit commands inside .husky/_/ by mistake, init rewrites those entries back to the short stub without --force.

Init flags

Flag Behavior
(none) Full setup: env files + Husky + hooks + package.json updates when applicable.
--env-only Only env / example-file merges — no Git hooks or Husky.
--husky Husky + hooks only — skips package.json merges. Use --workspace with --husky if you also need package.json updated again.
--force Replaces .env (when it is the merge target) and the resolved example file with the bundled template (destructive), and can overwrite existing hook files. Does not wholesale-replace .env.local.

When behavior differs

Situation What happens
Not in a git repository Env files under the current directory are updated; init reports that Git / Husky / hooks were skipped.
Monorepo (package not at repo root) Run init from the package folder that has package.json and depends on @verndale/ai-commit. Hooks stay at the repo root; generated scripts cd into your package first.
.env.local exists Ai-commit keys are merged only into .env.local; .env is not created or updated for that merge.
Without --force Missing keys are appended to the env merge target and example file; existing values are not wiped.

Command cheat sheet

pnpm add -D @verndale/ai-commit
pnpm exec ai-commit init
# Follow the printed "Next: run …" line if shown, then set OPENAI_API_KEY in .env or .env.local

Optional:

pnpm exec ai-commit init --env-only   # env only, no hooks
pnpm exec ai-commit init --husky      # hooks + Husky; skips package.json merge
pnpm exec ai-commit init --force      # overwrite env/hooks per flag rules

Environment variables

Variable Purpose
OPENAI_API_KEY Required for ai-commit run and for AI-backed prepare-commit-msg when you want generation.
COMMIT_AI_MODEL Optional model id (default gpt-4o-mini).

Load order: .env, then .env.local (later file wins on duplicate keys).

Comments: On merge, init may add a # @verndale/ai-commit — … line above assignments when missing; it does not remove existing comments.

Optional keys for other tools: PR_* for @verndale/ai-pr; RELEASE_NOTES_AI_* for tools/semantic-release-notes.cjs; use GH_TOKEN or GITHUB_TOKEN for GitHub API calls outside Actions.


Commit policy (v2)

  • Mandatory scope — Headers look like type(scope): Subject or type(scope)!: when breaking. Scope is derived from staged paths (lib/core/message-policy.js), with fallback from package.json (e.g. ai-commit).
  • Typesbuild, chore, ci, docs, feat, fix, perf, refactor, revert, style, test.
  • Subject — Imperative, Beams-style (first word capitalized), max 50 characters, no trailing period.
  • Body / footer — Wrap at 72 characters when present.
  • Issues — If branch or diff mentions #123, footers may add Refs #n / Closes #n (no invented numbers).
  • Breaking changes — Only when policy detects governance-related files (commitlint, Husky, this package’s rules/preset); otherwise ! and BREAKING CHANGE: are stripped.
  • Staged diff for AI — Lockfiles and common binary globs are excluded from the text sent to the model (lib/core/git.js); path detection still uses the full staged file list.

Semver: v2 tightens commitlint (mandatory scope, stricter lengths). If you extend this preset, review lib/rules.js and adjust overrides as needed.


CLI reference

Command Purpose
ai-commit run Build a message from the staged diff and run git commit.
ai-commit init Merge env files; configure Husky and hooks; update package.json when applicable. See Init flags.
ai-commit prepare-commit-msg <file> [source] Hook: fill an empty message; skips merge / squash.
ai-commit lint --edit <file> Hook: run commitlint with this package’s default config.

package.json script (example)

{
  "scripts": {
    "commit": "ai-commit run"
  }
}

Husky (manual setup)

pnpm exec ai-commit init sets up Husky for you. With Husky 9, Git only executes files under core.hooksPath (usually .husky/_/). Each of those must be a short stub that sources h; Husky then runs the matching script one level up, e.g. .husky/prepare-commit-msg, where your pnpm exec ai-commit … (or npx) commands live.

.husky/_/prepare-commit-msg (entry — stub only)

#!/usr/bin/env sh
. "$(dirname -- "$0")/h"

.husky/prepare-commit-msg (commands Husky runs)

#!/usr/bin/env sh
pnpm exec ai-commit prepare-commit-msg "$1" "$2"

.husky/_/commit-msg and .husky/commit-msg follow the same pattern (stub vs pnpm exec ai-commit lint --edit "$1").

If core.hooksPath is .husky (not .husky/_), use a single file per hook: source . "$(dirname -- "$0")/_/h", then the same pnpm exec ai-commit … lines init would generate.

Generated hooks use pnpm exec ai-commit when pnpm-lock.yaml exists at the package root; otherwise npx --no ai-commit. In a monorepo, the .husky/ scripts cd into the package directory first. Edit the scripts if you use another runner.

Default pre-commit: Husky’s init also writes .husky/pre-commit with pnpm test (or npm test / yarn test). That file is not always the one Git runs when core.hooksPath is .husky/_, but ai-commit init still removes that stock .husky/pre-commit when it matches the known template so it does not surprise you later. If you add other lines (e.g. lint-staged), the file is left unchanged.

Already using Husky? If .husky/_/h exists, npx husky@9 init is not run again. package.json is only amended for missing commit, prepare, or devDependencies.husky. Existing prepare-commit-msg and commit-msg hooks are not overwritten unless you use ai-commit init --force.


commitlint without a second install

Use ai-commit lint --edit from hooks (see above).

To extend the preset in your own commitlint.config.js:

module.exports = {
  extends: ["@verndale/ai-commit"],
  rules: {
    // optional overrides
  },
};

Shared constants (types, line limits):

const rules = require("@verndale/ai-commit/rules");

GitHub Actions (CI snippet)

Use commitlint in your workflow — nothing calls back to this repository’s pipelines. After pnpm add -D @verndale/ai-commit, add a root commitlint.config.cjs (or .js) that extends: ["@verndale/ai-commit"] as above. @commitlint/cli is a dependency of this package, so pnpm exec commitlint works after install.

Save as .github/workflows/commitlint.yml (or merge the job into an existing workflow). Adjust branches / branches-ignore if your default branch is not main.

name: Commit message lint

on:
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened, edited]
  push:
    branches-ignore:
      - main

jobs:
  commitlint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: "24.14.0"

      - name: Enable pnpm via Corepack
        run: corepack enable && corepack prepare pnpm@10.11.0 --activate

      - name: Get pnpm store path
        id: pnpm-cache
        run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT

      - name: Cache pnpm store
        uses: actions/cache@v4
        with:
          path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-store-

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Lint PR title (squash merge becomes the commit on main)
        if: github.event_name == 'pull_request'
        env:
          PR_TITLE: ${{ github.event.pull_request.title }}
        run: |
          printf '%s\n' "$PR_TITLE" | pnpm exec commitlint --verbose

      - name: Lint commit messages (PR range)
        if: github.event_name == 'pull_request'
        run: |
          pnpm exec commitlint \
            --from "${{ github.event.pull_request.base.sha }}" \
            --to "${{ github.event.pull_request.head.sha }}" \
            --verbose

      - name: Lint last commit (push)
        if: github.event_name == 'push'
        run: |
          pnpm exec commitlint --from=HEAD~1 --to=HEAD --verbose

Workflow notes

Topic Detail
Node Use a version that satisfies engines.node (see Requirements).
npm or Yarn Replace Corepack + pnpm with your install (npm ci, yarn install --immutable, etc.) and use npx --no commitlint or yarn exec commitlint.
Config path If commitlint cannot find your config, add --config path/to/commitlint.config.cjs to each invocation.
Same rules as hooks Matches .husky/commit-msg when it runs ai-commit lint --edit — both use the @verndale/ai-commit preset.

Contributing

Local development, workflows in this repo, and publishing are documented in CONTRIBUTING.md.


License

MIT — see LICENSE.

About

AI-assisted conventional commits with bundled commitlint — one install, aligned rules

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors