diff --git a/.env-example b/.env-example index 9204b82..c80b5dc 100644 --- a/.env-example +++ b/.env-example @@ -1,10 +1,14 @@ # Copy to .env and/or .env.local in your project root (do not commit secrets). +# ------------------------------------------------------------ +# @verndale/ai-commit (pnpm commit / ai-commit run) +# ------------------------------------------------------------ # @verndale/ai-commit — OPENAI_API_KEY: OpenAI API key for conventional commit messages (ai-commit run; optional for prepare-commit-msg with AI). OPENAI_API_KEY= -# @verndale/ai-commit — COMMIT_AI_MODEL: OpenAI model for commit messages (optional; default gpt-4o-mini). +# Optional — default is gpt-4o-mini # COMMIT_AI_MODEL= + # @verndale/ai-pr — GH_TOKEN: Set for local CLI runs; CI uses workflow env / secrets. Also reads GITHUB_TOKEN. GH_TOKEN= diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 833b8d2..555f739 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -67,4 +67,4 @@ jobs: PR_AI_ENDPOINT: ${{ vars.PR_AI_ENDPOINT }} PR_AI_MODEL: ${{ vars.PR_AI_MODEL }} PR_AI_API_KEY: ${{ secrets.PR_AI_API_KEY }} - run: pnpm open-pr + run: pnpm run pr:create diff --git a/README.md b/README.md index 973da06..d6b10c1 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Use **`ai-commit init --force`** to replace **`.env`** and **`.env-example`** wi - **`OPENAI_API_KEY`** — Required for `ai-commit run` (and for AI-filled `prepare-commit-msg` when you want the model). Optional `COMMIT_AI_MODEL` (default `gpt-4o-mini`). - **Shared env vars** — If another tool already documents **`OPENAI_API_KEY`** or **`COMMIT_AI_MODEL`**, **`ai-commit init`** adds its own `# @verndale/ai-commit — …` line immediately above the assignment when missing; it does not remove or replace existing comment lines. - The CLI loads **`.env`** then **`.env.local`** from the current working directory (project root); values in `.env.local` override `.env` for the same key. -- **Optional tooling:** `PR_*` env vars for [`@verndale/ai-pr`](https://www.npmjs.com/package/@verndale/ai-pr) (`pnpm open-pr` in this repo) / the **Create or update PR** workflow; `RELEASE_NOTES_AI_*` for [`tools/semantic-release-notes.cjs`](./tools/semantic-release-notes.cjs). Use a GitHub PAT as **`GH_TOKEN`** (or `GITHUB_TOKEN`) when calling the GitHub API outside Actions. +- **Optional tooling:** `PR_*` env vars for [`@verndale/ai-pr`](https://www.npmjs.com/package/@verndale/ai-pr) (`pnpm run pr:create` in this repo) / the **Create or update PR** workflow; `RELEASE_NOTES_AI_*` for [`tools/semantic-release-notes.cjs`](./tools/semantic-release-notes.cjs). Use a GitHub PAT as **`GH_TOKEN`** (or `GITHUB_TOKEN`) when calling the GitHub API outside Actions. ## Commit policy (v2) @@ -125,10 +125,10 @@ Copy **`.env-example`** to `.env` and/or `.env.local` and set **`OPENAI_API_KEY` | Workflow | Trigger | Purpose | | --- | --- | --- | | [`.github/workflows/commitlint.yml`](./.github/workflows/commitlint.yml) | PRs to `main`, pushes to non-`main` branches | Commitlint on PR range or last push commit | -| [`.github/workflows/pr.yml`](./.github/workflows/pr.yml) | Pushes (not `main`) and `workflow_dispatch` | Install deps, run **`pnpm open-pr`** ([**`@verndale/ai-pr`**](https://www.npmjs.com/package/@verndale/ai-pr)) — set **`PR_HEAD_BRANCH`** / **`PR_BASE_BRANCH`** in CI via env (workflow sets them). Use a PAT secret **`PR_BOT_TOKEN`** if branch protection requires it; otherwise document your org’s policy. | +| [`.github/workflows/pr.yml`](./.github/workflows/pr.yml) | Pushes (not `main`) and `workflow_dispatch` | Install deps, run **`pnpm run pr:create`** ([**`@verndale/ai-pr`**](https://www.npmjs.com/package/@verndale/ai-pr)) — set **`PR_HEAD_BRANCH`** / **`PR_BASE_BRANCH`** in CI via env (workflow sets them). Use a PAT secret **`PR_BOT_TOKEN`** if branch protection requires it; otherwise document your org’s policy. | | [`.github/workflows/release.yml`](./.github/workflows/release.yml) | Push to **`main`** (including when a PR merges) | **`semantic-release`** — version bump, `CHANGELOG.md`, git tag, npm publish (with provenance), GitHub Release | -Optional **`pnpm open-pr`** locally: set **`GH_TOKEN`** (or **`GITHUB_TOKEN`**) and branch overrides **`PR_BASE_BRANCH`** / **`PR_HEAD_BRANCH`** as needed. +Optional **`pnpm run pr:create`** locally: set **`GH_TOKEN`** (or **`GITHUB_TOKEN`**) and branch overrides **`PR_BASE_BRANCH`** / **`PR_HEAD_BRANCH`** as needed. ## Publishing (maintainers) diff --git a/asdasd b/asdasd new file mode 100644 index 0000000..95d09f2 --- /dev/null +++ b/asdasd @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/lib/init-env.js b/lib/init-env.js index ef20941..83ead5c 100644 --- a/lib/init-env.js +++ b/lib/init-env.js @@ -5,13 +5,57 @@ const fs = require("fs"); /** Detect our doc line so we do not duplicate or replace other packages’ comments. */ const MARKER_PREFIX = "# @verndale/ai-commit — "; +const SECTION_DIVIDER = "# ------------------------------------------------------------"; +const SECTION_TITLE = "# @verndale/ai-commit (pnpm commit / ai-commit run)"; +const SECTION_HEADER = [SECTION_DIVIDER, SECTION_TITLE, SECTION_DIVIDER]; + +const SUBSECTION_OPTIONAL_MODEL = "# Optional — default is gpt-4o-mini"; + const DOC_OPENAI = [ `${MARKER_PREFIX}OPENAI_API_KEY: OpenAI API key for conventional commit messages (ai-commit run; optional for prepare-commit-msg with AI).`, ]; -const DOC_COMMIT_MODEL = [ - `${MARKER_PREFIX}COMMIT_AI_MODEL: OpenAI model for commit messages (optional; default gpt-4o-mini).`, -]; +/** + * @param {string} text + * @returns {boolean} + */ +function hasAiCommitSectionHeader(text) { + return ( + text.includes(SECTION_DIVIDER) && + text.includes("@verndale/ai-commit (pnpm commit / ai-commit run)") + ); +} + +/** + * True when COMMIT_AI_MODEL is already documented (long marker, subsection, or legacy block). + * @param {string[]} lines + * @returns {boolean} + */ +function hasCommitModelNotes(lines) { + if (hasOurDocForKey(lines, "COMMIT_AI_MODEL")) { + return true; + } + const commitIdx = lines.findIndex( + (line) => + /^\s*COMMIT_AI_MODEL\s*=/.test(line) || /^\s*#\s*COMMIT_AI_MODEL\s*=/.test(line), + ); + if (commitIdx === -1) { + return false; + } + for (let i = commitIdx - 1; i >= 0 && i >= commitIdx - 12; i--) { + const t = lines[i].trim(); + if (t === "") { + continue; + } + if (t === SUBSECTION_OPTIONAL_MODEL.trim()) { + return true; + } + if (lines[i].includes(`${MARKER_PREFIX}COMMIT_AI_MODEL:`)) { + return true; + } + } + return false; +} /** * Keys assigned on non-comment lines (`KEY=value` or `export KEY=value`). @@ -38,27 +82,6 @@ function hasOurDocForKey(lines, key) { return lines.some((line) => line.includes(needle)); } -/** - * Insert ai-commit doc lines immediately before an assignment line, without changing - * existing comments above that line (we insert after those lines, before the key line). - * @param {string[]} lines mutable - * @param {RegExp} assignmentRegex - * @param {string[]} docLines - * @param {string} key for marker check - * @returns {boolean} whether lines were mutated - */ -function injectDocBeforeAssignment(lines, assignmentRegex, docLines, key) { - if (hasOurDocForKey(lines, key)) { - return false; - } - const idx = lines.findIndex((line) => assignmentRegex.test(line)); - if (idx === -1) { - return false; - } - lines.splice(idx, 0, ...docLines); - return true; -} - /** * For keys already present (possibly with another package’s comments), add our doc line(s) * above the assignment if missing. Does not remove or edit existing comment lines. @@ -69,24 +92,27 @@ function injectAiCommitDocsForExistingKeys(content) { const lines = content.split(/\r?\n/); let changed = false; - if ( - injectDocBeforeAssignment( - lines, - /^\s*OPENAI_API_KEY\s*=/, - DOC_OPENAI, - "OPENAI_API_KEY", - ) - ) { - changed = true; + if (!hasOurDocForKey(lines, "OPENAI_API_KEY")) { + const idx = lines.findIndex((line) => /^\s*OPENAI_API_KEY\s*=/.test(line)); + if (idx !== -1) { + const insert = hasAiCommitSectionHeader(lines.join("\n")) + ? [DOC_OPENAI[0]] + : [...SECTION_HEADER, DOC_OPENAI[0]]; + lines.splice(idx, 0, ...insert); + changed = true; + } } - if (!hasOurDocForKey(lines, "COMMIT_AI_MODEL")) { + if (!hasCommitModelNotes(lines)) { let idx = lines.findIndex((line) => /^\s*COMMIT_AI_MODEL\s*=/.test(line)); if (idx === -1) { idx = lines.findIndex((line) => /^\s*#\s*COMMIT_AI_MODEL\s*=/.test(line)); } if (idx !== -1) { - lines.splice(idx, 0, ...DOC_COMMIT_MODEL); + const insert = hasAiCommitSectionHeader(lines.join("\n")) + ? [SUBSECTION_OPTIONAL_MODEL] + : [...SECTION_HEADER, "", SUBSECTION_OPTIONAL_MODEL]; + lines.splice(idx, 0, ...insert); changed = true; } } @@ -106,17 +132,43 @@ function buildAiCommitEnvAppend(existing) { keys.has("COMMIT_AI_MODEL") || /^\s*#\s*COMMIT_AI_MODEL\s*=/m.test(existing) || /^\s*COMMIT_AI_MODEL\s*=/m.test(existing); - const parts = []; - if (!keys.has("OPENAI_API_KEY")) { - parts.push(`${DOC_OPENAI[0]}\nOPENAI_API_KEY=\n`); + const needOpenai = !keys.has("OPENAI_API_KEY"); + const needCommit = !keys.has("COMMIT_AI_MODEL") && !hasCommitPlaceholder; + const hasSection = hasAiCommitSectionHeader(existing); + + if (!needOpenai && !needCommit) { + return null; } - if (!keys.has("COMMIT_AI_MODEL") && !hasCommitPlaceholder) { - parts.push(`${DOC_COMMIT_MODEL[0]}\n# COMMIT_AI_MODEL=\n`); + + if (needOpenai && needCommit) { + return [ + ...SECTION_HEADER, + DOC_OPENAI[0], + "OPENAI_API_KEY=", + "", + SUBSECTION_OPTIONAL_MODEL, + "# COMMIT_AI_MODEL=", + "", + "", + ].join("\n"); } - if (parts.length === 0) { - return null; + + if (needOpenai) { + return [...SECTION_HEADER, DOC_OPENAI[0], "OPENAI_API_KEY=", ""].join("\n"); + } + + if (hasSection) { + return [SUBSECTION_OPTIONAL_MODEL, "# COMMIT_AI_MODEL=", "", ""].join("\n"); } - return parts.join("\n"); + + return [ + ...SECTION_HEADER, + "", + SUBSECTION_OPTIONAL_MODEL, + "# COMMIT_AI_MODEL=", + "", + "", + ].join("\n"); } /** @@ -124,7 +176,7 @@ function buildAiCommitEnvAppend(existing) { * @param {string} destPath * @param {string} bundledPath * @param {{ force?: boolean }} [options] - * @returns {{ kind: 'replaced' | 'wrote' | 'merged' | 'unchanged' }} + * @returns {{ kind: 'replaced' | 'wrote' | 'unchanged' }} */ function mergeAiCommitEnvFile(destPath, bundledPath, options = {}) { const { force = false } = options;