Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pnpm exec ai-commit init
| Action | Detail |
| --- | --- |
| Roots | **Package root** — walks up from the current directory toward the git root and uses the first directory that has **`package.json`** (if none, uses cwd). **Git root** — `git rev-parse --show-toplevel`. Env files and **`package.json`** use the package root; hooks use the git root. |
| Env files | Merges **`.env`** and the **example env file** (see below). Keys already set in **`.env.local`** are treated as satisfied for the **`.env`** merge only (same as runtime load order). Init does not write **`.env.local`**. |
| Env files | Merges ai-commit keys into **`.env.local`** when that file exists; otherwise into **`.env`** (creates **`.env`** from the bundled template if missing). **`.env`** is not used when **`.env.local`** is present. Also merges the **example env file** (see below). **`--force`** does not wholesale-replace **`.env.local`** (append / docs only). |
| Example file | Uses **`.env.example`** if it exists; else **`.env-example`** if it exists; else creates **`.env.example`**. If both dot forms exist, init uses **`.env.example`** and prints a warning. The **bundled** template in the package remains [`.env-example`](.env-example) (hyphen). |
| Husky | Runs **`npx husky@9 init`** at the **git root** if **`husky.sh`** is missing under the resolved hooks directory. |
| Hooks directory | **`core.hooksPath`** relative to the git root when set; otherwise **`<git-root>/.husky`**. Falls back to **`.husky`** at the git root with a warning if the config path is invalid or outside the repo. |
Expand All @@ -64,17 +64,17 @@ Set **`OPENAI_API_KEY`** in **`.env`** and/or **`.env.local`**. Duplicate keys:
| *(none)* | Full setup: env files + Husky + hooks + `package.json` updates (when applicable). |
| `--env-only` | You only want env / example-file updates—no Git hooks. |
| `--husky` | Hooks + Husky only; skips **`package.json`** changes. Combine with **`--workspace`** if you need **`package.json`** merged again. |
| `--force` | Replace **`.env`** and the resolved example file (see table above) with the bundled template **(destructive)** and/or overwrite existing Husky hook files. |
| `--force` | Replace **`.env`** (when that is the merge target) and the resolved example file with the bundled template **(destructive)** and/or overwrite existing Husky hook files. Does **not** wholesale-replace **`.env.local`** (merge/append only). |

**Edge cases**

| Situation | Behavior |
| --- | --- |
| Not in a git repo | Init updates env files only (under cwd) and reports that Git/Husky were skipped. |
| Monorepo (package not at git root) | Run **`ai-commit init`** from the app folder that has **`package.json`** and the dependency. Hooks live at the repo root; hook scripts change into the package directory before running **`ai-commit`**. |
| **`.env.local`** | **`OPENAI_API_KEY`** / **`COMMIT_AI_MODEL`** there count as already present when merging **`.env`**, so init will not add duplicate placeholders for those keys. |
| **`.env.local`** | If this file exists, init merges ai-commit keys **only** here and does not create or update **`.env`** (see [Env files](#2-run-init) row). |
| Bundled vs consumer example name | The npm package ships **`.env-example`** (hyphen) as the template source; the file init merges into on disk follows **`.env.example`** first, then **`.env-example`**, then default **`.env.example`**. |
| Without **`--force`** | Missing example file is created (**`.env.example`** when neither exists); otherwise missing ai-commit keys are **appended** to **`.env`** and the example file without wiping them. |
| Without **`--force`** | Missing example file is created (**`.env.example`** when neither dot form exists); otherwise missing ai-commit keys are **appended** to the env merge target (**`.env.local`** or **`.env`**) and the example file without wiping them. |

---

Expand Down Expand Up @@ -129,7 +129,7 @@ pnpm exec ai-commit init --force
| Command | Purpose |
| --- | --- |
| **`ai-commit run`** | Build a message from the staged diff and run **`git commit`**. |
| **`ai-commit init`** | Env merge (**`.env`** + resolved example file; **`.env.local`** satisfies keys for the **`.env`** merge only), Husky at git root if needed, **`package.json`** at package root, hooks in **`core.hooksPath`** or **`.husky`**. See [flags](#init-flags-and-shortcuts). |
| **`ai-commit init`** | Env merge into **`.env.local`** or **`.env`** (see [Env files](#2-run-init)) plus resolved example file; Husky at git root if needed, **`package.json`** at package root, hooks in **`core.hooksPath`** or **`.husky`**. See [flags](#init-flags-and-shortcuts). |
| **`ai-commit prepare-commit-msg <file> [source]`** | Hook: fill an empty message; skips `merge` / `squash`. |
| **`ai-commit lint --edit <file>`** | Hook: commitlint with this package’s default config. |

Expand Down
35 changes: 20 additions & 15 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const {
hasStagedChanges,
commitFromFile,
} = require("../lib/core/git.js");
const { mergeAiCommitEnvFile, parseDotenvAssignedKeys } = require("../lib/init-env.js");
const { mergeAiCommitEnvFile } = require("../lib/init-env.js");
const { resolveEnvExamplePath, findPackageRoot } = require("../lib/init-paths.js");
const {
detectPackageExec,
Expand Down Expand Up @@ -45,7 +45,7 @@ Usage:

Commands:
run Generate a message from the staged diff and run git commit.
init Merge env, then Husky + package.json + hooks (from a git repo). \`--env-only\` stops after env files. \`--husky\` skips package.json. \`--force\` replaces \`.env\` / example env file / hooks (example path: existing \`.env.example\` or \`.env-example\`, default \`.env.example\`).
init Merge env, then Husky + package.json + hooks (from a git repo). \`--env-only\` stops after env files. \`--husky\` skips package.json. Env merge targets \`.env.local\` when that file exists, else \`.env\`. \`--force\` replaces \`.env\` / example file / hooks (not a wholesale replace of \`.env.local\`).
prepare-commit-msg Git hook: fill an empty commit message file (merge/squash skipped).
lint Run commitlint with the package default config (for commit-msg hook).

Expand Down Expand Up @@ -99,14 +99,8 @@ function cmdInit(argv) {
const gitRoot = inGit ? getGitRoot(cwd) : null;
const packageRoot = findPackageRoot(cwd, gitRoot);

const extraAssignedKeys = new Set();
const envLocalPath = path.join(packageRoot, ".env.local");
if (fs.existsSync(envLocalPath)) {
const localContent = fs.readFileSync(envLocalPath, "utf8");
for (const k of parseDotenvAssignedKeys(localContent)) {
extraAssignedKeys.add(k);
}
}
const envPath = path.join(packageRoot, ".env");

if (
inGit &&
Expand All @@ -118,12 +112,21 @@ function cmdInit(argv) {
);
}

const envDest = path.join(packageRoot, ".env");
const envResult = mergeAiCommitEnvFile(envDest, bundledExamplePath, {
force,
extraAssignedKeys,
/** When `.env.local` exists it is the only env merge target (no `.env` created or updated). */
const envMergePath = fs.existsSync(envLocalPath) ? envLocalPath : envPath;
const mergeEnvIntoLocal =
path.resolve(envMergePath) === path.resolve(envLocalPath);
/** Never `--force`-replace `.env.local` with the bundled template (would wipe secrets). */
const envForce = force && !mergeEnvIntoLocal;
if (force && mergeEnvIntoLocal) {
process.stderr.write(
"note: --force does not replace .env.local with the bundled template; ai-commit keys are merged (append / docs) only.\n",
);
}
const envResult = mergeAiCommitEnvFile(envMergePath, bundledExamplePath, {
force: envForce,
});
const envRel = path.relative(cwd, envDest) || ".env";
const envRel = path.relative(cwd, envMergePath) || path.basename(envMergePath);
switch (envResult.kind) {
case "replaced":
process.stdout.write(`Replaced ${envRel} with bundled template (--force).\n`);
Expand All @@ -136,7 +139,9 @@ function cmdInit(argv) {
break;
case "unchanged":
process.stdout.write(
`No missing @verndale/ai-commit keys in ${envRel}; left unchanged. Use --force to replace the file with the bundled template.\n`,
mergeEnvIntoLocal
? `No missing @verndale/ai-commit keys in ${envRel}; left unchanged.\n`
: `No missing @verndale/ai-commit keys in ${envRel}; left unchanged. Use --force to replace the file with the bundled template.\n`,
);
break;
default:
Expand Down
Loading