@verndale/ai-commit 2.5.0 → 2.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -12
- package/bin/cli.js +63 -20
- package/lib/core/git.js +50 -0
- package/lib/init-env.js +11 -4
- package/lib/init-paths.js +65 -0
- package/lib/init-workspace.js +11 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ pnpm add -D @verndale/ai-commit
|
|
|
25
25
|
|
|
26
26
|
## Setup
|
|
27
27
|
|
|
28
|
-
Do these **in order** from
|
|
28
|
+
Do these **in order** from the **app directory** where **`@verndale/ai-commit`** is installed — the folder that contains **`package.json`** (in a monorepo, that is often **not** the git repository root). Init updates **`.env`** / the example file there and installs hooks at the **git root** (with a `cd` in hook scripts when those paths differ).
|
|
29
29
|
|
|
30
30
|
### 1. Install the package
|
|
31
31
|
|
|
@@ -41,10 +41,13 @@ pnpm exec ai-commit init
|
|
|
41
41
|
|
|
42
42
|
| Action | Detail |
|
|
43
43
|
| --- | --- |
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
44
|
+
| 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. |
|
|
45
|
+
| 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). |
|
|
46
|
+
| 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). |
|
|
47
|
+
| Husky | Runs **`npx husky@9 init`** at the **git root** if **`husky.sh`** is missing under the resolved hooks directory. |
|
|
48
|
+
| 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. |
|
|
49
|
+
| `package.json` | Adds missing **`commit`**, **`prepare`**, **`husky`** entries when **`package.json`** exists at the package root. |
|
|
50
|
+
| Hooks | Writes **`prepare-commit-msg`** and **`commit-msg`** in the hooks directory. If package root ≠ git root, each hook **`cd`s** into the package directory before **`pnpm exec ai-commit`** / **`npx`**. |
|
|
48
51
|
|
|
49
52
|
If **`package.json`** changed, run **`pnpm install`** (or `npm install`) again.
|
|
50
53
|
|
|
@@ -59,17 +62,19 @@ Set **`OPENAI_API_KEY`** in **`.env`** and/or **`.env.local`**. Duplicate keys:
|
|
|
59
62
|
| Flag | Use when |
|
|
60
63
|
| --- | --- |
|
|
61
64
|
| *(none)* | Full setup: env files + Husky + hooks + `package.json` updates (when applicable). |
|
|
62
|
-
| `--env-only` | You only want env /
|
|
65
|
+
| `--env-only` | You only want env / example-file updates—no Git hooks. |
|
|
63
66
|
| `--husky` | Hooks + Husky only; skips **`package.json`** changes. Combine with **`--workspace`** if you need **`package.json`** merged again. |
|
|
64
|
-
| `--force` | Replace **`.env`** and
|
|
67
|
+
| `--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). |
|
|
65
68
|
|
|
66
69
|
**Edge cases**
|
|
67
70
|
|
|
68
71
|
| Situation | Behavior |
|
|
69
72
|
| --- | --- |
|
|
70
|
-
| Not in a git repo | Init updates env files only and reports that Git/Husky were skipped. |
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
+
| Not in a git repo | Init updates env files only (under cwd) and reports that Git/Husky were skipped. |
|
|
74
|
+
| 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`**. |
|
|
75
|
+
| **`.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). |
|
|
76
|
+
| 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`**. |
|
|
77
|
+
| 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. |
|
|
73
78
|
|
|
74
79
|
---
|
|
75
80
|
|
|
@@ -124,7 +129,7 @@ pnpm exec ai-commit init --force
|
|
|
124
129
|
| Command | Purpose |
|
|
125
130
|
| --- | --- |
|
|
126
131
|
| **`ai-commit run`** | Build a message from the staged diff and run **`git commit`**. |
|
|
127
|
-
| **`ai-commit init`** | Env merge
|
|
132
|
+
| **`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). |
|
|
128
133
|
| **`ai-commit prepare-commit-msg <file> [source]`** | Hook: fill an empty message; skips `merge` / `squash`. |
|
|
129
134
|
| **`ai-commit lint --edit <file>`** | Hook: commitlint with this package’s default config. |
|
|
130
135
|
|
|
@@ -164,7 +169,7 @@ pnpm exec ai-commit prepare-commit-msg "$1" "$2"
|
|
|
164
169
|
pnpm exec ai-commit lint --edit "$1"
|
|
165
170
|
```
|
|
166
171
|
|
|
167
|
-
Hooks from **`init`** use **`pnpm exec ai-commit`** when **`pnpm-lock.yaml`** exists
|
|
172
|
+
Hooks from **`init`** use **`pnpm exec ai-commit`** when **`pnpm-lock.yaml`** exists in the **package root**; otherwise **`npx --no ai-commit`**. In a monorepo, generated hooks **`cd`** from the git root into that package directory first. Edit the files if you use another runner.
|
|
168
173
|
|
|
169
174
|
**Already using Husky?** If **`.husky/_/husky.sh`** exists, **`init`** does not run **`npx husky@9 init`**. **`package.json`** is only amended for missing **`commit`**, **`prepare`**, or **`devDependencies.husky`**. Existing **`.husky/prepare-commit-msg`** and **`.husky/commit-msg`** are not overwritten unless you use **`ai-commit init --force`**.
|
|
170
175
|
|
package/bin/cli.js
CHANGED
|
@@ -11,10 +11,13 @@ const { generateAndValidate } = require("../lib/core/generate.js");
|
|
|
11
11
|
const {
|
|
12
12
|
assertInGitRepo,
|
|
13
13
|
isInGitRepo,
|
|
14
|
+
getGitRoot,
|
|
15
|
+
resolveGitHooksDir,
|
|
14
16
|
hasStagedChanges,
|
|
15
17
|
commitFromFile,
|
|
16
18
|
} = require("../lib/core/git.js");
|
|
17
19
|
const { mergeAiCommitEnvFile } = require("../lib/init-env.js");
|
|
20
|
+
const { resolveEnvExamplePath, findPackageRoot } = require("../lib/init-paths.js");
|
|
18
21
|
const {
|
|
19
22
|
detectPackageExec,
|
|
20
23
|
hookScript,
|
|
@@ -42,7 +45,7 @@ Usage:
|
|
|
42
45
|
|
|
43
46
|
Commands:
|
|
44
47
|
run Generate a message from the staged diff and run git commit.
|
|
45
|
-
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\` /
|
|
48
|
+
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\`).
|
|
46
49
|
prepare-commit-msg Git hook: fill an empty commit message file (merge/squash skipped).
|
|
47
50
|
lint Run commitlint with the package default config (for commit-msg hook).
|
|
48
51
|
|
|
@@ -86,15 +89,44 @@ function cmdInit(argv) {
|
|
|
86
89
|
const cwd = process.cwd();
|
|
87
90
|
/** Full package.json merge: default on, or `--workspace`; off for `--husky` alone (legacy). */
|
|
88
91
|
const mergePackageJson = !husky || workspace;
|
|
89
|
-
const
|
|
92
|
+
const bundledExamplePath = path.join(__dirname, "..", ".env-example");
|
|
90
93
|
|
|
91
|
-
if (!fs.existsSync(
|
|
94
|
+
if (!fs.existsSync(bundledExamplePath)) {
|
|
92
95
|
throw new Error("Missing bundled .env-example (corrupt install?).");
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
+
const inGit = isInGitRepo(cwd);
|
|
99
|
+
const gitRoot = inGit ? getGitRoot(cwd) : null;
|
|
100
|
+
const packageRoot = findPackageRoot(cwd, gitRoot);
|
|
101
|
+
|
|
102
|
+
const envLocalPath = path.join(packageRoot, ".env.local");
|
|
103
|
+
const envPath = path.join(packageRoot, ".env");
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
inGit &&
|
|
107
|
+
gitRoot &&
|
|
108
|
+
path.resolve(packageRoot) !== path.resolve(gitRoot)
|
|
109
|
+
) {
|
|
110
|
+
process.stdout.write(
|
|
111
|
+
`Note: env files are updated under ${packageRoot}; Git hooks use the repository root ${gitRoot}.\n`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** When `.env.local` exists it is the only env merge target (no `.env` created or updated). */
|
|
116
|
+
const envMergePath = fs.existsSync(envLocalPath) ? envLocalPath : envPath;
|
|
117
|
+
const mergeEnvIntoLocal =
|
|
118
|
+
path.resolve(envMergePath) === path.resolve(envLocalPath);
|
|
119
|
+
/** Never `--force`-replace `.env.local` with the bundled template (would wipe secrets). */
|
|
120
|
+
const envForce = force && !mergeEnvIntoLocal;
|
|
121
|
+
if (force && mergeEnvIntoLocal) {
|
|
122
|
+
process.stderr.write(
|
|
123
|
+
"note: --force does not replace .env.local with the bundled template; ai-commit keys are merged (append / docs) only.\n",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const envResult = mergeAiCommitEnvFile(envMergePath, bundledExamplePath, {
|
|
127
|
+
force: envForce,
|
|
128
|
+
});
|
|
129
|
+
const envRel = path.relative(cwd, envMergePath) || path.basename(envMergePath);
|
|
98
130
|
switch (envResult.kind) {
|
|
99
131
|
case "replaced":
|
|
100
132
|
process.stdout.write(`Replaced ${envRel} with bundled template (--force).\n`);
|
|
@@ -107,16 +139,18 @@ function cmdInit(argv) {
|
|
|
107
139
|
break;
|
|
108
140
|
case "unchanged":
|
|
109
141
|
process.stdout.write(
|
|
110
|
-
|
|
142
|
+
mergeEnvIntoLocal
|
|
143
|
+
? `No missing @verndale/ai-commit keys in ${envRel}; left unchanged.\n`
|
|
144
|
+
: `No missing @verndale/ai-commit keys in ${envRel}; left unchanged. Use --force to replace the file with the bundled template.\n`,
|
|
111
145
|
);
|
|
112
146
|
break;
|
|
113
147
|
default:
|
|
114
148
|
break;
|
|
115
149
|
}
|
|
116
150
|
|
|
117
|
-
const envExampleDest =
|
|
118
|
-
const exResult = mergeAiCommitEnvFile(envExampleDest,
|
|
119
|
-
const exRel = path.relative(cwd, envExampleDest) ||
|
|
151
|
+
const envExampleDest = resolveEnvExamplePath(packageRoot);
|
|
152
|
+
const exResult = mergeAiCommitEnvFile(envExampleDest, bundledExamplePath, { force });
|
|
153
|
+
const exRel = path.relative(cwd, envExampleDest) || path.basename(envExampleDest);
|
|
120
154
|
switch (exResult.kind) {
|
|
121
155
|
case "replaced":
|
|
122
156
|
process.stdout.write(`Replaced ${exRel} with bundled template (--force).\n`);
|
|
@@ -140,17 +174,24 @@ function cmdInit(argv) {
|
|
|
140
174
|
return;
|
|
141
175
|
}
|
|
142
176
|
|
|
143
|
-
if (!
|
|
177
|
+
if (!inGit) {
|
|
144
178
|
process.stdout.write(
|
|
145
|
-
"Not a git repository (or git unavailable); skipped Husky and package.json.
|
|
179
|
+
"Not a git repository (or git unavailable); skipped Husky and package.json hooks. Run init from your app directory inside a git repo (with package.json there) for full setup.\n",
|
|
180
|
+
);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!gitRoot) {
|
|
184
|
+
process.stderr.write(
|
|
185
|
+
"warning: could not resolve git repository root; skipped Husky and hooks.\n",
|
|
146
186
|
);
|
|
147
187
|
return;
|
|
148
188
|
}
|
|
149
189
|
|
|
150
|
-
|
|
190
|
+
let { dir: huskyDir } = resolveGitHooksDir(gitRoot);
|
|
191
|
+
const huskyHelper = path.join(huskyDir, "_", "husky.sh");
|
|
151
192
|
|
|
152
193
|
if (!fs.existsSync(huskyHelper)) {
|
|
153
|
-
const r = runHuskyInit(
|
|
194
|
+
const r = runHuskyInit(gitRoot);
|
|
154
195
|
if (!r.ok) {
|
|
155
196
|
process.stderr.write(
|
|
156
197
|
r.error
|
|
@@ -160,14 +201,15 @@ function cmdInit(argv) {
|
|
|
160
201
|
process.exit(1);
|
|
161
202
|
}
|
|
162
203
|
process.stdout.write("Ran `npx husky@9 init`.\n");
|
|
204
|
+
huskyDir = resolveGitHooksDir(gitRoot).dir;
|
|
163
205
|
} else {
|
|
164
206
|
process.stdout.write(
|
|
165
|
-
|
|
207
|
+
`Husky already initialized (found ${path.join(huskyDir, "_", "husky.sh")}); skipped \`npx husky@9 init\`.\n`,
|
|
166
208
|
);
|
|
167
209
|
}
|
|
168
210
|
|
|
169
211
|
if (mergePackageJson) {
|
|
170
|
-
const pkgPath = path.join(
|
|
212
|
+
const pkgPath = path.join(packageRoot, "package.json");
|
|
171
213
|
if (fs.existsSync(pkgPath)) {
|
|
172
214
|
const { changed } = mergePackageJsonForAiCommit(pkgPath);
|
|
173
215
|
if (changed) {
|
|
@@ -177,16 +219,17 @@ function cmdInit(argv) {
|
|
|
177
219
|
}
|
|
178
220
|
warnIfPrepareMissingHusky(pkgPath);
|
|
179
221
|
} else {
|
|
180
|
-
process.stdout.write(
|
|
222
|
+
process.stdout.write(
|
|
223
|
+
"No package.json found walking up to the git root; skipped package.json merge (hooks still written).\n",
|
|
224
|
+
);
|
|
181
225
|
}
|
|
182
226
|
}
|
|
183
227
|
|
|
184
|
-
const huskyDir = path.join(cwd, ".husky");
|
|
185
228
|
if (!fs.existsSync(huskyDir)) {
|
|
186
229
|
fs.mkdirSync(huskyDir, { recursive: true });
|
|
187
230
|
}
|
|
188
231
|
|
|
189
|
-
const execPrefix = detectPackageExec(
|
|
232
|
+
const execPrefix = detectPackageExec(packageRoot);
|
|
190
233
|
const preparePath = path.join(huskyDir, "prepare-commit-msg");
|
|
191
234
|
const commitMsgPath = path.join(huskyDir, "commit-msg");
|
|
192
235
|
|
|
@@ -194,7 +237,7 @@ function cmdInit(argv) {
|
|
|
194
237
|
[preparePath, "prepare-commit-msg"],
|
|
195
238
|
[commitMsgPath, "commit-msg"],
|
|
196
239
|
]) {
|
|
197
|
-
const body = hookScript(execPrefix, hookKind);
|
|
240
|
+
const body = hookScript(packageRoot, gitRoot, execPrefix, hookKind);
|
|
198
241
|
if (fs.existsSync(hookPath) && !force) {
|
|
199
242
|
process.stderr.write(`Skipped ${path.relative(cwd, hookPath)} (already exists). Use --force to overwrite.\n`);
|
|
200
243
|
} else {
|
package/lib/core/git.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const path = require("path");
|
|
3
4
|
const { execFileSync } = require("child_process");
|
|
4
5
|
|
|
5
6
|
/** Large staged diffs must not throw ENOBUFS (align with generous buffer in prior tooling). */
|
|
@@ -31,6 +32,53 @@ function isInGitRepo(cwd = process.cwd()) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} cwd
|
|
37
|
+
* @returns {string | null} Absolute git root, or null if not in a repo / git unavailable
|
|
38
|
+
*/
|
|
39
|
+
function getGitRoot(cwd = process.cwd()) {
|
|
40
|
+
try {
|
|
41
|
+
return execGit(["rev-parse", "--show-toplevel"], { cwd }).trim();
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the directory where Git runs hooks (`core.hooksPath` relative to git root, or `.husky`).
|
|
49
|
+
* Falls back to `<gitRoot>/.husky` with a warning if `core.hooksPath` is missing, empty, or outside the repo.
|
|
50
|
+
* @param {string} gitRoot
|
|
51
|
+
* @returns {{ dir: string, warned: boolean }}
|
|
52
|
+
*/
|
|
53
|
+
function resolveGitHooksDir(gitRoot) {
|
|
54
|
+
const defaultDir = path.join(gitRoot, ".husky");
|
|
55
|
+
let raw = "";
|
|
56
|
+
try {
|
|
57
|
+
raw = execGit(["config", "--get", "core.hooksPath"], { cwd: gitRoot }).trim();
|
|
58
|
+
} catch {
|
|
59
|
+
return { dir: defaultDir, warned: false };
|
|
60
|
+
}
|
|
61
|
+
if (!raw) {
|
|
62
|
+
return { dir: defaultDir, warned: false };
|
|
63
|
+
}
|
|
64
|
+
const unquoted = raw.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
|
|
65
|
+
const hooksPath = unquoted;
|
|
66
|
+
const resolved = path.isAbsolute(hooksPath)
|
|
67
|
+
? path.normalize(hooksPath)
|
|
68
|
+
: path.resolve(gitRoot, hooksPath);
|
|
69
|
+
const rootResolved = path.resolve(gitRoot);
|
|
70
|
+
const hooksResolved = path.resolve(resolved);
|
|
71
|
+
const rel = path.relative(rootResolved, hooksResolved);
|
|
72
|
+
const outside = rel.startsWith("..") || path.isAbsolute(rel);
|
|
73
|
+
if (outside) {
|
|
74
|
+
process.stderr.write(
|
|
75
|
+
"warning: core.hooksPath points outside the repository or is invalid; using .husky at the git root.\n",
|
|
76
|
+
);
|
|
77
|
+
return { dir: defaultDir, warned: true };
|
|
78
|
+
}
|
|
79
|
+
return { dir: resolved, warned: false };
|
|
80
|
+
}
|
|
81
|
+
|
|
34
82
|
function assertInGitRepo(cwd = process.cwd()) {
|
|
35
83
|
if (!isInGitRepo(cwd)) {
|
|
36
84
|
const err = new Error("Not a git repository (or git not available).");
|
|
@@ -97,6 +145,8 @@ module.exports = {
|
|
|
97
145
|
DIFF_EXCLUDE_PATHSPECS,
|
|
98
146
|
execGit,
|
|
99
147
|
isInGitRepo,
|
|
148
|
+
getGitRoot,
|
|
149
|
+
resolveGitHooksDir,
|
|
100
150
|
assertInGitRepo,
|
|
101
151
|
getStagedDiff,
|
|
102
152
|
getChangedFiles,
|
package/lib/init-env.js
CHANGED
|
@@ -139,11 +139,18 @@ function injectAiCommitDocsForExistingKeys(content) {
|
|
|
139
139
|
/**
|
|
140
140
|
* Build text to append so OPENAI_API_KEY / COMMIT_AI_MODEL placeholders exist.
|
|
141
141
|
* Returns null if nothing to add.
|
|
142
|
+
* Keys listed in `extraAssignedKeys` (e.g. from `.env.local`) count as already satisfied.
|
|
142
143
|
* @param {string} existing
|
|
144
|
+
* @param {Set<string> | undefined} [extraAssignedKeys]
|
|
143
145
|
* @returns {string | null}
|
|
144
146
|
*/
|
|
145
|
-
function buildAiCommitEnvAppend(existing) {
|
|
147
|
+
function buildAiCommitEnvAppend(existing, extraAssignedKeys) {
|
|
146
148
|
const keys = parseDotenvAssignedKeys(existing);
|
|
149
|
+
if (extraAssignedKeys && extraAssignedKeys.size > 0) {
|
|
150
|
+
for (const k of extraAssignedKeys) {
|
|
151
|
+
keys.add(k);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
147
154
|
const hasCommitPlaceholder =
|
|
148
155
|
keys.has("COMMIT_AI_MODEL") ||
|
|
149
156
|
/^\s*#\s*COMMIT_AI_MODEL\s*=/m.test(existing) ||
|
|
@@ -186,11 +193,11 @@ function buildAiCommitEnvAppend(existing) {
|
|
|
186
193
|
* Merge bundled ai-commit env keys into a file. Never removes existing lines.
|
|
187
194
|
* @param {string} destPath
|
|
188
195
|
* @param {string} bundledPath
|
|
189
|
-
* @param {{ force?: boolean }} [options]
|
|
196
|
+
* @param {{ force?: boolean, extraAssignedKeys?: Set<string> }} [options]
|
|
190
197
|
* @returns {{ kind: 'replaced' | 'wrote' | 'merged' | 'unchanged' }}
|
|
191
198
|
*/
|
|
192
199
|
function mergeAiCommitEnvFile(destPath, bundledPath, options = {}) {
|
|
193
|
-
const { force = false } = options;
|
|
200
|
+
const { force = false, extraAssignedKeys } = options;
|
|
194
201
|
const bundled = fs.readFileSync(bundledPath, "utf8");
|
|
195
202
|
|
|
196
203
|
if (force) {
|
|
@@ -209,7 +216,7 @@ function mergeAiCommitEnvFile(destPath, bundledPath, options = {}) {
|
|
|
209
216
|
}
|
|
210
217
|
|
|
211
218
|
let text = injectAiCommitDocsForExistingKeys(existing);
|
|
212
|
-
const append = buildAiCommitEnvAppend(text);
|
|
219
|
+
const append = buildAiCommitEnvAppend(text, extraAssignedKeys);
|
|
213
220
|
if (append !== null) {
|
|
214
221
|
const sep = text.endsWith("\n") ? "" : "\n";
|
|
215
222
|
text = `${text}${sep}${append}`;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pick the consumer example env file: prefer `.env.example`, else `.env-example`, else default `.env.example`.
|
|
8
|
+
* If both dot forms exist, merges target `.env.example` and emits a one-line stderr warning.
|
|
9
|
+
* @param {string} dir Absolute or resolved directory (e.g. package root)
|
|
10
|
+
* @returns {string} Destination path for the example/template merge
|
|
11
|
+
*/
|
|
12
|
+
function resolveEnvExamplePath(dir) {
|
|
13
|
+
const dotExample = path.join(dir, ".env.example");
|
|
14
|
+
const dotHyphen = path.join(dir, ".env-example");
|
|
15
|
+
const hasExample = fs.existsSync(dotExample);
|
|
16
|
+
const hasHyphen = fs.existsSync(dotHyphen);
|
|
17
|
+
if (hasExample && hasHyphen) {
|
|
18
|
+
process.stderr.write(
|
|
19
|
+
"warning: both .env.example and .env-example exist; using .env.example. Remove or consolidate the other file if redundant.\n",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
if (hasExample) {
|
|
23
|
+
return dotExample;
|
|
24
|
+
}
|
|
25
|
+
if (hasHyphen) {
|
|
26
|
+
return dotHyphen;
|
|
27
|
+
}
|
|
28
|
+
return dotExample;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Walk from `cwd` up toward `gitRoot` (inclusive); first directory with `package.json` wins.
|
|
33
|
+
* If `gitRoot` is null, returns `cwd` (no upward walk).
|
|
34
|
+
* If none found before/at git root, returns `cwd`.
|
|
35
|
+
* @param {string} cwd
|
|
36
|
+
* @param {string | null} gitRoot
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function findPackageRoot(cwd, gitRoot) {
|
|
40
|
+
const cwdResolved = path.resolve(cwd);
|
|
41
|
+
if (!gitRoot) {
|
|
42
|
+
return cwdResolved;
|
|
43
|
+
}
|
|
44
|
+
const rootResolved = path.resolve(gitRoot);
|
|
45
|
+
let dir = cwdResolved;
|
|
46
|
+
for (;;) {
|
|
47
|
+
if (fs.existsSync(path.join(dir, "package.json"))) {
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
if (dir === rootResolved) {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
const parent = path.dirname(dir);
|
|
54
|
+
if (parent === dir) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
dir = parent;
|
|
58
|
+
}
|
|
59
|
+
return cwdResolved;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
resolveEnvExamplePath,
|
|
64
|
+
findPackageRoot,
|
|
65
|
+
};
|
package/lib/init-workspace.js
CHANGED
|
@@ -18,18 +18,27 @@ function detectPackageExec(cwd) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
+
* @param {string} packageRoot Absolute package directory (where lockfile / ai-commit dep live)
|
|
22
|
+
* @param {string} gitRoot Absolute git repository root
|
|
21
23
|
* @param {string} execPrefix from detectPackageExec
|
|
22
24
|
* @param {"prepare-commit-msg" | "commit-msg"} hook
|
|
23
25
|
*/
|
|
24
|
-
function hookScript(execPrefix, hook) {
|
|
26
|
+
function hookScript(packageRoot, gitRoot, execPrefix, hook) {
|
|
25
27
|
const cmd =
|
|
26
28
|
hook === "prepare-commit-msg"
|
|
27
29
|
? `${execPrefix} ai-commit prepare-commit-msg "$1" "$2"`
|
|
28
30
|
: `${execPrefix} ai-commit lint --edit "$1"`;
|
|
31
|
+
const pkgNorm = path.resolve(packageRoot);
|
|
32
|
+
const gitNorm = path.resolve(gitRoot);
|
|
33
|
+
let cdBlock = "";
|
|
34
|
+
if (pkgNorm !== gitNorm) {
|
|
35
|
+
const rel = path.relative(gitNorm, pkgNorm).split(path.sep).join("/");
|
|
36
|
+
cdBlock = `root="$(git rev-parse --show-toplevel)"\ncd "$root/${rel}"\n`;
|
|
37
|
+
}
|
|
29
38
|
return `#!/usr/bin/env sh
|
|
30
39
|
. "$(dirname -- "$0")/_/husky.sh"
|
|
31
40
|
|
|
32
|
-
${cmd}
|
|
41
|
+
${cdBlock}${cmd}
|
|
33
42
|
`;
|
|
34
43
|
}
|
|
35
44
|
|