hermes-git 0.3.1 → 0.3.5
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 +82 -73
- package/dist/index.js +413 -50
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,6 +42,8 @@ hermes wip
|
|
|
42
42
|
|
|
43
43
|
No magic. Every command shows exactly what git operations it runs.
|
|
44
44
|
|
|
45
|
+
Run `hermes` with no arguments to see the full command reference and available workflows.
|
|
46
|
+
|
|
45
47
|
---
|
|
46
48
|
|
|
47
49
|
## Installation
|
|
@@ -78,7 +80,7 @@ hermes config set provider gemini
|
|
|
78
80
|
hermes config set gemini-key AIza...
|
|
79
81
|
```
|
|
80
82
|
|
|
81
|
-
Verify
|
|
83
|
+
Verify:
|
|
82
84
|
|
|
83
85
|
```bash
|
|
84
86
|
hermes config list
|
|
@@ -93,7 +95,7 @@ hermes config list
|
|
|
93
95
|
npm install -g hermes-git
|
|
94
96
|
hermes config setup
|
|
95
97
|
|
|
96
|
-
# 2. Initialize your project (optional
|
|
98
|
+
# 2. Initialize your project (optional — enables team config sharing)
|
|
97
99
|
cd your-project
|
|
98
100
|
hermes init
|
|
99
101
|
|
|
@@ -105,13 +107,26 @@ hermes start "user authentication"
|
|
|
105
107
|
|
|
106
108
|
## Commands
|
|
107
109
|
|
|
110
|
+
### `hermes update`
|
|
111
|
+
|
|
112
|
+
Update hermes to the latest version.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
hermes update # check and install if a newer version exists
|
|
116
|
+
hermes update --check # check only, don't install
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Auto-detects your package manager (npm, bun, or pnpm).
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
108
123
|
### `hermes config`
|
|
109
124
|
|
|
110
125
|
Manage API keys and provider settings.
|
|
111
126
|
|
|
112
127
|
```bash
|
|
113
|
-
hermes config setup #
|
|
114
|
-
hermes config list #
|
|
128
|
+
hermes config setup # interactive wizard
|
|
129
|
+
hermes config list # show current config (keys masked, sources shown)
|
|
115
130
|
hermes config set provider openai
|
|
116
131
|
hermes config set openai-key sk-...
|
|
117
132
|
hermes config get provider
|
|
@@ -122,6 +137,52 @@ Config is stored in `~/.config/hermes/config.json`. You can also use environment
|
|
|
122
137
|
|
|
123
138
|
---
|
|
124
139
|
|
|
140
|
+
### `hermes guard`
|
|
141
|
+
|
|
142
|
+
Scan staged files for secrets and sensitive content before committing.
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
hermes guard
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Scans every staged file for:
|
|
149
|
+
|
|
150
|
+
- **Sensitive filenames** — `.env`, `id_rsa`, `*.pem`, `credentials.json`, `google-services.json`, etc.
|
|
151
|
+
- **API keys** — Anthropic, OpenAI, Google, AWS, GitHub, Stripe, SendGrid, Twilio
|
|
152
|
+
- **Private key headers** — `-----BEGIN PRIVATE KEY-----` and variants
|
|
153
|
+
- **Database URLs** with embedded credentials — `postgres://user:pass@host`
|
|
154
|
+
- **Hardcoded passwords/tokens** — common assignment patterns
|
|
155
|
+
|
|
156
|
+
Findings are categorised as `BLOCKED` (definite secret) or `WARN` (suspicious). The AI explains each finding and what to do about it, then you choose: abort, unstage the flagged files, or proceed anyway.
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
BLOCKED src/config.ts
|
|
160
|
+
● Anthropic API key line 12
|
|
161
|
+
apiKey: "sk-a...****",
|
|
162
|
+
Rotate at: https://console.anthropic.com/settings/keys
|
|
163
|
+
|
|
164
|
+
What this means:
|
|
165
|
+
This key gives anyone with repo access full billing access to your
|
|
166
|
+
Anthropic account. Rotate it immediately and load it from an
|
|
167
|
+
environment variable instead.
|
|
168
|
+
|
|
169
|
+
? Blocked secrets found. What do you want to do?
|
|
170
|
+
❯ Abort — I will fix these before committing
|
|
171
|
+
Unstage the flagged files and continue
|
|
172
|
+
Proceed anyway (I know what I'm doing)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Install as a git pre-commit hook** so it runs automatically on every commit:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
hermes guard install-hook # installs to .git/hooks/pre-commit
|
|
179
|
+
hermes guard uninstall-hook # removes it
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
In hook mode the scan is non-interactive: prints findings to stderr and exits 1 on any blocker.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
125
186
|
### `hermes plan "<intent>"`
|
|
126
187
|
|
|
127
188
|
Analyze repo state and propose a safe Git plan. **Makes no changes.**
|
|
@@ -154,7 +215,7 @@ hermes sync
|
|
|
154
215
|
hermes sync --from develop
|
|
155
216
|
```
|
|
156
217
|
|
|
157
|
-
|
|
218
|
+
Evaluates whether rebase or merge is safer given your branch state and explains before executing.
|
|
158
219
|
|
|
159
220
|
---
|
|
160
221
|
|
|
@@ -171,55 +232,6 @@ Decides commit vs stash based on what's safest in your current state.
|
|
|
171
232
|
|
|
172
233
|
---
|
|
173
234
|
|
|
174
|
-
### `hermes guard`
|
|
175
|
-
|
|
176
|
-
Scan staged files for secrets and sensitive content before committing.
|
|
177
|
-
|
|
178
|
-
```bash
|
|
179
|
-
hermes guard
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
Hermes scans every staged file for:
|
|
183
|
-
|
|
184
|
-
- **Sensitive filenames** — `.env`, `id_rsa`, `*.pem`, `credentials.json`, `google-services.json`, etc.
|
|
185
|
-
- **API keys** — Anthropic, OpenAI, Google, AWS, GitHub, Stripe, SendGrid, Twilio
|
|
186
|
-
- **Private key headers** — `-----BEGIN PRIVATE KEY-----` and variants
|
|
187
|
-
- **Database URLs** with embedded credentials — `postgres://user:pass@host`
|
|
188
|
-
- **Hardcoded passwords/tokens** — common assignment patterns
|
|
189
|
-
|
|
190
|
-
Findings are categorized as `BLOCKED` (definite secret) or `WARN` (suspicious). The AI explains each finding and what to do about it. Then you choose: abort, unstage the flagged files, or proceed anyway.
|
|
191
|
-
|
|
192
|
-
```
|
|
193
|
-
BLOCKED src/config.ts
|
|
194
|
-
● Anthropic API key line 12
|
|
195
|
-
apiKey: "sk-a...****",
|
|
196
|
-
Rotate at: https://console.anthropic.com/settings/keys
|
|
197
|
-
● Database URL with credentials line 15
|
|
198
|
-
dbUrl: "post...****prod.db.internal/app"
|
|
199
|
-
|
|
200
|
-
What this means:
|
|
201
|
-
The Anthropic API key on line 12 would give anyone with repository
|
|
202
|
-
access full billing access to your Anthropic account. Rotate it
|
|
203
|
-
immediately and use process.env.ANTHROPIC_API_KEY instead.
|
|
204
|
-
...
|
|
205
|
-
|
|
206
|
-
? Blocked secrets found. What do you want to do?
|
|
207
|
-
❯ Abort — I will fix these before committing
|
|
208
|
-
Unstage the flagged files and continue
|
|
209
|
-
Proceed anyway (I know what I'm doing)
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
**Install as a git pre-commit hook** so it runs automatically on every commit:
|
|
213
|
-
|
|
214
|
-
```bash
|
|
215
|
-
hermes guard install-hook # installs to .git/hooks/pre-commit
|
|
216
|
-
hermes guard uninstall-hook # removes it
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
In hook mode (`--hook`), the scan is non-interactive: it prints findings to stderr and exits 1 on any blocker.
|
|
220
|
-
|
|
221
|
-
---
|
|
222
|
-
|
|
223
235
|
### `hermes conflict explain`
|
|
224
236
|
|
|
225
237
|
Understand why a conflict exists.
|
|
@@ -246,15 +258,17 @@ For each file: shows a proposed resolution, lets you accept, edit manually, or s
|
|
|
246
258
|
|
|
247
259
|
### `hermes workflow <name>`
|
|
248
260
|
|
|
249
|
-
One-command workflows for common patterns.
|
|
261
|
+
One-command workflows for common patterns. Available workflows are shown when you run `hermes` with no arguments.
|
|
250
262
|
|
|
251
263
|
```bash
|
|
252
264
|
hermes workflow pr-ready # fetch → rebase → push --force-with-lease
|
|
253
265
|
hermes workflow daily-sync # fetch all → show status → suggest next action
|
|
254
266
|
hermes workflow quick-commit # generate commit message from staged diff
|
|
255
|
-
hermes workflow list # show
|
|
267
|
+
hermes workflow list # show all workflows including project-specific
|
|
256
268
|
```
|
|
257
269
|
|
|
270
|
+
Define project-specific workflows in `.hermes/config.json` and they appear automatically in the help output.
|
|
271
|
+
|
|
258
272
|
---
|
|
259
273
|
|
|
260
274
|
### `hermes worktree new "<task>"`
|
|
@@ -273,8 +287,8 @@ hermes worktree new "fix memory leak"
|
|
|
273
287
|
Initialize project-level config (`.hermes/config.json`). Commit this to share branch patterns and workflows with your team.
|
|
274
288
|
|
|
275
289
|
```bash
|
|
276
|
-
hermes init #
|
|
277
|
-
hermes init --quick #
|
|
290
|
+
hermes init # interactive
|
|
291
|
+
hermes init --quick # use defaults
|
|
278
292
|
```
|
|
279
293
|
|
|
280
294
|
---
|
|
@@ -301,6 +315,8 @@ Hermes resolves config in this priority order:
|
|
|
301
315
|
| `.env` file in current dir | `ANTHROPIC_API_KEY=sk-ant-...` |
|
|
302
316
|
| `~/.config/hermes/config.json` | set via `hermes config set` |
|
|
303
317
|
|
|
318
|
+
Environment variables always win — useful for CI and Docker environments where you don't want a config file.
|
|
319
|
+
|
|
304
320
|
**Supported env vars:**
|
|
305
321
|
|
|
306
322
|
| Variable | Description |
|
|
@@ -326,9 +342,9 @@ If `HERMES_PROVIDER` is not set, Hermes auto-detects by using whichever key it f
|
|
|
326
342
|
|
|
327
343
|
1. **Reads your repo state** — branch, commits, dirty files, conflicts, remote tracking
|
|
328
344
|
2. **Sends context + intent to an AI** — using your configured provider
|
|
329
|
-
3. **Validates the response** — all returned commands must start with `git
|
|
345
|
+
3. **Validates the response** — all returned commands must start with `git`; destructive flags are blocked
|
|
330
346
|
4. **Executes with display** — shows every command before running it
|
|
331
|
-
5. **You
|
|
347
|
+
5. **You stay in control** — interactive prompts for anything irreversible
|
|
332
348
|
|
|
333
349
|
---
|
|
334
350
|
|
|
@@ -350,30 +366,23 @@ If `HERMES_PROVIDER` is not set, Hermes auto-detects by using whichever key it f
|
|
|
350
366
|
hermes config setup
|
|
351
367
|
```
|
|
352
368
|
|
|
353
|
-
**Wrong provider
|
|
369
|
+
**Wrong provider being used**
|
|
354
370
|
|
|
355
371
|
```bash
|
|
356
372
|
hermes config set provider anthropic
|
|
357
|
-
hermes config list #
|
|
373
|
+
hermes config list # check sources — env vars override saved config
|
|
358
374
|
```
|
|
359
375
|
|
|
360
|
-
**Key
|
|
376
|
+
**Key set but not working**
|
|
361
377
|
|
|
362
378
|
```bash
|
|
363
|
-
#
|
|
364
|
-
hermes config list
|
|
365
|
-
|
|
366
|
-
# Environment variables override saved config
|
|
367
|
-
# Check for conflicting vars:
|
|
368
|
-
echo $ANTHROPIC_API_KEY
|
|
379
|
+
hermes config list # shows value and where it came from (env / .env / config)
|
|
369
380
|
```
|
|
370
381
|
|
|
371
|
-
**
|
|
382
|
+
**Update to latest**
|
|
372
383
|
|
|
373
384
|
```bash
|
|
374
|
-
hermes
|
|
375
|
-
hermes config list
|
|
376
|
-
git status
|
|
385
|
+
hermes update
|
|
377
386
|
```
|
|
378
387
|
|
|
379
388
|
---
|
package/dist/index.js
CHANGED
|
@@ -37589,9 +37589,11 @@ function initCommand(program2) {
|
|
|
37589
37589
|
await mkdir3(".hermes", { recursive: true });
|
|
37590
37590
|
await writeFile4(".hermes/config.json", JSON.stringify(config, null, 2));
|
|
37591
37591
|
await mkdir3(".hermes/backups", { recursive: true });
|
|
37592
|
-
await
|
|
37593
|
-
.
|
|
37594
|
-
`
|
|
37592
|
+
const added = await updateGitignore();
|
|
37593
|
+
if (added.length > 0) {
|
|
37594
|
+
console.log(`
|
|
37595
|
+
\uD83D\uDCDD Added to .gitignore: ${added.join(", ")}`);
|
|
37596
|
+
}
|
|
37595
37597
|
displaySuccess("Hermes initialized successfully!");
|
|
37596
37598
|
console.log(`
|
|
37597
37599
|
\uD83D\uDCC4 Configuration saved to .hermes/config.json`);
|
|
@@ -37734,23 +37736,38 @@ async function interactiveConfig(repoInfo) {
|
|
|
37734
37736
|
}
|
|
37735
37737
|
};
|
|
37736
37738
|
}
|
|
37737
|
-
|
|
37738
|
-
|
|
37739
|
-
|
|
37740
|
-
|
|
37741
|
-
|
|
37742
|
-
|
|
37743
|
-
|
|
37744
|
-
|
|
37745
|
-
|
|
37739
|
+
var GITIGNORE_ENTRIES = [
|
|
37740
|
+
{ pattern: ".env", comment: null },
|
|
37741
|
+
{ pattern: ".env.*", comment: null },
|
|
37742
|
+
{ pattern: "!.env.example", comment: null },
|
|
37743
|
+
{ pattern: ".hermes/backups/", comment: null },
|
|
37744
|
+
{ pattern: ".hermes/stats.json", comment: null }
|
|
37745
|
+
];
|
|
37746
|
+
async function updateGitignore() {
|
|
37747
|
+
const { appendFile, readFile: rf } = await import("fs/promises");
|
|
37748
|
+
const gitignorePath = ".gitignore";
|
|
37749
|
+
let existing = "";
|
|
37750
|
+
if (existsSync4(gitignorePath)) {
|
|
37751
|
+
try {
|
|
37752
|
+
existing = await rf(gitignorePath, "utf-8");
|
|
37753
|
+
} catch {}
|
|
37754
|
+
}
|
|
37755
|
+
const existingLines = existing.split(`
|
|
37756
|
+
`).map((l) => l.trim());
|
|
37757
|
+
const toAdd = GITIGNORE_ENTRIES.filter((e) => !existingLines.includes(e.pattern));
|
|
37758
|
+
if (toAdd.length === 0)
|
|
37759
|
+
return [];
|
|
37760
|
+
const block = `
|
|
37746
37761
|
# Hermes
|
|
37747
|
-
` +
|
|
37748
|
-
|
|
37749
|
-
|
|
37750
|
-
|
|
37751
|
-
|
|
37752
|
-
|
|
37753
|
-
|
|
37762
|
+
` + toAdd.map((e) => e.pattern).join(`
|
|
37763
|
+
`) + `
|
|
37764
|
+
`;
|
|
37765
|
+
if (existsSync4(gitignorePath)) {
|
|
37766
|
+
await appendFile(gitignorePath, block);
|
|
37767
|
+
} else {
|
|
37768
|
+
await writeFile4(gitignorePath, block.trimStart());
|
|
37769
|
+
}
|
|
37770
|
+
return toAdd.map((e) => e.pattern);
|
|
37754
37771
|
}
|
|
37755
37772
|
|
|
37756
37773
|
// src/commands/stats.ts
|
|
@@ -38737,32 +38754,348 @@ function buildInstallCommand(pm) {
|
|
|
38737
38754
|
}
|
|
38738
38755
|
}
|
|
38739
38756
|
|
|
38757
|
+
// src/commands/commit.ts
|
|
38758
|
+
import { exec as exec6 } from "child_process";
|
|
38759
|
+
import { promisify as promisify6 } from "util";
|
|
38760
|
+
var execAsync6 = promisify6(exec6);
|
|
38761
|
+
async function getStagedFiles2() {
|
|
38762
|
+
try {
|
|
38763
|
+
const { stdout } = await execAsync6("git diff --cached --name-status");
|
|
38764
|
+
return stdout.trim().split(`
|
|
38765
|
+
`).filter(Boolean).map((line) => {
|
|
38766
|
+
const [status, ...rest] = line.split("\t");
|
|
38767
|
+
return { status: status.trim(), path: rest.join("\t").trim() };
|
|
38768
|
+
});
|
|
38769
|
+
} catch {
|
|
38770
|
+
return [];
|
|
38771
|
+
}
|
|
38772
|
+
}
|
|
38773
|
+
async function getUnstagedFiles() {
|
|
38774
|
+
try {
|
|
38775
|
+
const { stdout } = await execAsync6("git status --porcelain");
|
|
38776
|
+
return stdout.trim().split(`
|
|
38777
|
+
`).filter(Boolean).map((line) => {
|
|
38778
|
+
const status = line.slice(0, 2).trim();
|
|
38779
|
+
const path5 = line.slice(3).trim();
|
|
38780
|
+
return { status, path: path5 };
|
|
38781
|
+
}).filter(({ status }) => status === "??" || status[0] === " " || status.length === 1);
|
|
38782
|
+
} catch {
|
|
38783
|
+
return [];
|
|
38784
|
+
}
|
|
38785
|
+
}
|
|
38786
|
+
async function getRecentBranchCommits(n = 5) {
|
|
38787
|
+
try {
|
|
38788
|
+
const { stdout } = await execAsync6(`git log --oneline -${n} --no-merges 2>/dev/null`);
|
|
38789
|
+
return stdout.trim();
|
|
38790
|
+
} catch {
|
|
38791
|
+
return "";
|
|
38792
|
+
}
|
|
38793
|
+
}
|
|
38794
|
+
function statusLabel(s) {
|
|
38795
|
+
const map = {
|
|
38796
|
+
M: source_default.yellow("modified "),
|
|
38797
|
+
A: source_default.green("new file "),
|
|
38798
|
+
D: source_default.red("deleted "),
|
|
38799
|
+
R: source_default.blue("renamed "),
|
|
38800
|
+
C: source_default.blue("copied "),
|
|
38801
|
+
"??": source_default.dim("untracked"),
|
|
38802
|
+
" M": source_default.yellow("modified "),
|
|
38803
|
+
" D": source_default.red("deleted ")
|
|
38804
|
+
};
|
|
38805
|
+
return map[s] ?? source_default.dim(s.padEnd(9));
|
|
38806
|
+
}
|
|
38807
|
+
function divider() {
|
|
38808
|
+
console.log(source_default.dim(" " + "─".repeat(54)));
|
|
38809
|
+
}
|
|
38810
|
+
async function analyzeChanges(branch, stagedDiff, stagedStat, recentCommits) {
|
|
38811
|
+
const prompt2 = `You are an expert Git historian. Analyze these staged changes and produce a structured commit analysis.
|
|
38812
|
+
|
|
38813
|
+
Branch: ${branch}
|
|
38814
|
+
Recent commits on this branch:
|
|
38815
|
+
${recentCommits || "(none — first commit)"}
|
|
38816
|
+
|
|
38817
|
+
Staged diff summary:
|
|
38818
|
+
${stagedStat}
|
|
38819
|
+
|
|
38820
|
+
Full staged diff (truncated to 12000 chars):
|
|
38821
|
+
${stagedDiff.slice(0, 12000)}
|
|
38822
|
+
|
|
38823
|
+
Return RAW JSON ONLY — no markdown, no code fences, just the JSON object:
|
|
38824
|
+
{
|
|
38825
|
+
"summary": "2-3 sentences: what was changed and the likely intent behind it",
|
|
38826
|
+
"concerns": ["array of concerns — mixed unrelated changes, debug/temp code, large binary, TODO left in, test missing, etc. Empty array if none."],
|
|
38827
|
+
"message": "conventional commit: type(scope): subject (max 72 chars). Be specific.",
|
|
38828
|
+
"body": "optional multi-line body explaining WHY (not WHAT). Empty string if the subject is self-explanatory.",
|
|
38829
|
+
"alternatives": ["two alternative commit messages if the intent is ambiguous"]
|
|
38830
|
+
}
|
|
38831
|
+
|
|
38832
|
+
Rules for message:
|
|
38833
|
+
- type: feat | fix | refactor | docs | test | chore | style | perf | build | ci
|
|
38834
|
+
- scope: the primary module/component affected (omit if changes are cross-cutting)
|
|
38835
|
+
- subject: imperative mood, no period, lowercase after colon
|
|
38836
|
+
- be specific — "add OAuth login" not "add feature"`;
|
|
38837
|
+
const raw = await getAISuggestion(prompt2);
|
|
38838
|
+
const cleaned = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
|
|
38839
|
+
try {
|
|
38840
|
+
return JSON.parse(cleaned);
|
|
38841
|
+
} catch {
|
|
38842
|
+
return {
|
|
38843
|
+
summary: "Could not parse analysis.",
|
|
38844
|
+
concerns: [],
|
|
38845
|
+
message: cleaned.split(`
|
|
38846
|
+
`)[0].slice(0, 72),
|
|
38847
|
+
body: "",
|
|
38848
|
+
alternatives: []
|
|
38849
|
+
};
|
|
38850
|
+
}
|
|
38851
|
+
}
|
|
38852
|
+
function commitCommand(program2) {
|
|
38853
|
+
program2.command("commit").description("Analyze staged changes with AI and craft a commit message").option("-a, --all", "Stage all tracked changes before analysis").option("--no-body", "Omit the commit body even if one is suggested").action(async (options) => {
|
|
38854
|
+
const start = Date.now();
|
|
38855
|
+
try {
|
|
38856
|
+
const repoState = await getRepoState();
|
|
38857
|
+
if (options.all) {
|
|
38858
|
+
displayStep("git add -u");
|
|
38859
|
+
await execAsync6("git add -u");
|
|
38860
|
+
}
|
|
38861
|
+
const staged = await getStagedFiles2();
|
|
38862
|
+
if (staged.length === 0) {
|
|
38863
|
+
console.log(source_default.yellow(`
|
|
38864
|
+
Nothing staged to commit.`));
|
|
38865
|
+
console.log(source_default.dim(" Use `git add <file>` or run `hermes commit --all` to stage tracked changes.\n"));
|
|
38866
|
+
process.exit(0);
|
|
38867
|
+
}
|
|
38868
|
+
const unstaged = await getUnstagedFiles();
|
|
38869
|
+
const recentCommits = await getRecentBranchCommits(5);
|
|
38870
|
+
console.log();
|
|
38871
|
+
console.log(source_default.bold(` Staged (${staged.length} file${staged.length === 1 ? "" : "s"}):`));
|
|
38872
|
+
for (const f of staged) {
|
|
38873
|
+
console.log(` ${statusLabel(f.status)} ${source_default.white(f.path)}`);
|
|
38874
|
+
}
|
|
38875
|
+
const relevantUnstaged = unstaged.filter((u) => !staged.some((s) => s.path === u.path));
|
|
38876
|
+
if (relevantUnstaged.length > 0) {
|
|
38877
|
+
console.log();
|
|
38878
|
+
console.log(source_default.dim(` Not staged (${relevantUnstaged.length}):`));
|
|
38879
|
+
for (const f of relevantUnstaged.slice(0, 8)) {
|
|
38880
|
+
console.log(` ${statusLabel(f.status)} ${source_default.dim(f.path)}`);
|
|
38881
|
+
}
|
|
38882
|
+
if (relevantUnstaged.length > 8) {
|
|
38883
|
+
console.log(source_default.dim(` ...and ${relevantUnstaged.length - 8} more`));
|
|
38884
|
+
}
|
|
38885
|
+
}
|
|
38886
|
+
const { name, model } = await getActiveProvider();
|
|
38887
|
+
console.log();
|
|
38888
|
+
console.log(` Analyzing... ${source_default.dim(`[${name} / ${model}]`)}`);
|
|
38889
|
+
const [stagedDiff, stagedStat] = await Promise.all([
|
|
38890
|
+
execAsync6("git diff --cached").then((r) => r.stdout),
|
|
38891
|
+
execAsync6("git diff --cached --stat").then((r) => r.stdout)
|
|
38892
|
+
]);
|
|
38893
|
+
const analysis = await analyzeChanges(repoState.currentBranch, stagedDiff, stagedStat, recentCommits);
|
|
38894
|
+
console.log();
|
|
38895
|
+
divider();
|
|
38896
|
+
console.log();
|
|
38897
|
+
console.log(" " + source_default.bold("Understanding"));
|
|
38898
|
+
console.log();
|
|
38899
|
+
const words = analysis.summary.split(" ");
|
|
38900
|
+
let line = " ";
|
|
38901
|
+
for (const w of words) {
|
|
38902
|
+
if (line.length + w.length > 62) {
|
|
38903
|
+
console.log(source_default.white(line));
|
|
38904
|
+
line = " " + w + " ";
|
|
38905
|
+
} else {
|
|
38906
|
+
line += w + " ";
|
|
38907
|
+
}
|
|
38908
|
+
}
|
|
38909
|
+
if (line.trim())
|
|
38910
|
+
console.log(source_default.white(line));
|
|
38911
|
+
if (analysis.concerns.length > 0) {
|
|
38912
|
+
console.log();
|
|
38913
|
+
for (const c of analysis.concerns) {
|
|
38914
|
+
console.log(` ${source_default.yellow("⚠")} ${source_default.yellow(c)}`);
|
|
38915
|
+
}
|
|
38916
|
+
}
|
|
38917
|
+
console.log();
|
|
38918
|
+
divider();
|
|
38919
|
+
console.log();
|
|
38920
|
+
const hasBody = options.body !== false && analysis.body.trim().length > 0;
|
|
38921
|
+
console.log(" " + source_default.bold("Proposed commit"));
|
|
38922
|
+
console.log();
|
|
38923
|
+
console.log(" " + source_default.cyan.bold(analysis.message));
|
|
38924
|
+
if (hasBody) {
|
|
38925
|
+
console.log();
|
|
38926
|
+
analysis.body.split(`
|
|
38927
|
+
`).forEach((l) => {
|
|
38928
|
+
console.log(" " + source_default.dim(l));
|
|
38929
|
+
});
|
|
38930
|
+
}
|
|
38931
|
+
console.log();
|
|
38932
|
+
let finalMessage = analysis.message;
|
|
38933
|
+
let finalBody = hasBody ? analysis.body : "";
|
|
38934
|
+
let committed = false;
|
|
38935
|
+
while (!committed) {
|
|
38936
|
+
const choices = [
|
|
38937
|
+
{ name: "Commit", value: "commit" },
|
|
38938
|
+
{ name: "Edit message", value: "edit" }
|
|
38939
|
+
];
|
|
38940
|
+
if (analysis.alternatives?.length > 0) {
|
|
38941
|
+
choices.push({ name: "See alternative messages", value: "alts" });
|
|
38942
|
+
}
|
|
38943
|
+
if (relevantUnstaged.length > 0) {
|
|
38944
|
+
choices.push({ name: "Stage more files first", value: "stage" });
|
|
38945
|
+
}
|
|
38946
|
+
choices.push({ name: "Cancel", value: "cancel" });
|
|
38947
|
+
const { action } = await esm_default12.prompt([
|
|
38948
|
+
{
|
|
38949
|
+
type: "list",
|
|
38950
|
+
name: "action",
|
|
38951
|
+
message: "What would you like to do?",
|
|
38952
|
+
choices
|
|
38953
|
+
}
|
|
38954
|
+
]);
|
|
38955
|
+
if (action === "cancel") {
|
|
38956
|
+
console.log(source_default.dim(`
|
|
38957
|
+
Cancelled. Changes remain staged.
|
|
38958
|
+
`));
|
|
38959
|
+
await recordCommand("commit", [], Date.now() - start, false);
|
|
38960
|
+
return;
|
|
38961
|
+
}
|
|
38962
|
+
if (action === "alts") {
|
|
38963
|
+
console.log();
|
|
38964
|
+
analysis.alternatives.forEach((alt, i) => {
|
|
38965
|
+
console.log(` ${source_default.dim(`${i + 1}.`)} ${source_default.cyan(alt)}`);
|
|
38966
|
+
});
|
|
38967
|
+
console.log();
|
|
38968
|
+
const { pick } = await esm_default12.prompt([
|
|
38969
|
+
{
|
|
38970
|
+
type: "list",
|
|
38971
|
+
name: "pick",
|
|
38972
|
+
message: "Use an alternative?",
|
|
38973
|
+
choices: [
|
|
38974
|
+
...analysis.alternatives.map((a, i) => ({ name: a, value: `${i}` })),
|
|
38975
|
+
{ name: "Keep original", value: "keep" }
|
|
38976
|
+
]
|
|
38977
|
+
}
|
|
38978
|
+
]);
|
|
38979
|
+
if (pick !== "keep") {
|
|
38980
|
+
finalMessage = analysis.alternatives[parseInt(pick, 10)];
|
|
38981
|
+
finalBody = "";
|
|
38982
|
+
console.log();
|
|
38983
|
+
console.log(" " + source_default.cyan.bold(finalMessage));
|
|
38984
|
+
console.log();
|
|
38985
|
+
}
|
|
38986
|
+
continue;
|
|
38987
|
+
}
|
|
38988
|
+
if (action === "edit") {
|
|
38989
|
+
const { edited } = await esm_default12.prompt([
|
|
38990
|
+
{
|
|
38991
|
+
type: "input",
|
|
38992
|
+
name: "edited",
|
|
38993
|
+
message: "Commit message:",
|
|
38994
|
+
default: finalMessage
|
|
38995
|
+
}
|
|
38996
|
+
]);
|
|
38997
|
+
finalMessage = edited.trim();
|
|
38998
|
+
finalBody = "";
|
|
38999
|
+
console.log();
|
|
39000
|
+
continue;
|
|
39001
|
+
}
|
|
39002
|
+
if (action === "stage") {
|
|
39003
|
+
const { files } = await esm_default12.prompt([
|
|
39004
|
+
{
|
|
39005
|
+
type: "checkbox",
|
|
39006
|
+
name: "files",
|
|
39007
|
+
message: "Select files to stage:",
|
|
39008
|
+
choices: relevantUnstaged.map((f) => ({
|
|
39009
|
+
name: `${statusLabel(f.status)} ${f.path}`,
|
|
39010
|
+
value: f.path
|
|
39011
|
+
}))
|
|
39012
|
+
}
|
|
39013
|
+
]);
|
|
39014
|
+
if (files.length > 0) {
|
|
39015
|
+
for (const f of files) {
|
|
39016
|
+
const cmd = `git add ${JSON.stringify(f)}`;
|
|
39017
|
+
displayStep(cmd);
|
|
39018
|
+
await execAsync6(cmd);
|
|
39019
|
+
}
|
|
39020
|
+
console.log(source_default.dim(`
|
|
39021
|
+
Staged ${files.length} file(s). Re-running analysis...
|
|
39022
|
+
`));
|
|
39023
|
+
const [newDiff, newStat] = await Promise.all([
|
|
39024
|
+
execAsync6("git diff --cached").then((r) => r.stdout),
|
|
39025
|
+
execAsync6("git diff --cached --stat").then((r) => r.stdout)
|
|
39026
|
+
]);
|
|
39027
|
+
const newAnalysis = await analyzeChanges(repoState.currentBranch, newDiff, newStat, recentCommits);
|
|
39028
|
+
analysis.summary = newAnalysis.summary;
|
|
39029
|
+
analysis.concerns = newAnalysis.concerns;
|
|
39030
|
+
analysis.message = newAnalysis.message;
|
|
39031
|
+
analysis.body = newAnalysis.body;
|
|
39032
|
+
analysis.alternatives = newAnalysis.alternatives;
|
|
39033
|
+
finalMessage = newAnalysis.message;
|
|
39034
|
+
finalBody = options.body !== false && newAnalysis.body.trim().length > 0 ? newAnalysis.body : "";
|
|
39035
|
+
console.log(" " + source_default.bold("Updated proposal"));
|
|
39036
|
+
console.log();
|
|
39037
|
+
console.log(" " + source_default.cyan.bold(finalMessage));
|
|
39038
|
+
if (finalBody) {
|
|
39039
|
+
console.log();
|
|
39040
|
+
finalBody.split(`
|
|
39041
|
+
`).forEach((l) => console.log(" " + source_default.dim(l)));
|
|
39042
|
+
}
|
|
39043
|
+
if (newAnalysis.concerns.length > 0) {
|
|
39044
|
+
console.log();
|
|
39045
|
+
newAnalysis.concerns.forEach((c) => {
|
|
39046
|
+
console.log(` ${source_default.yellow("⚠")} ${source_default.yellow(c)}`);
|
|
39047
|
+
});
|
|
39048
|
+
}
|
|
39049
|
+
console.log();
|
|
39050
|
+
}
|
|
39051
|
+
continue;
|
|
39052
|
+
}
|
|
39053
|
+
if (action === "commit") {
|
|
39054
|
+
const fullMessage = finalBody ? `${finalMessage}
|
|
39055
|
+
|
|
39056
|
+
${finalBody}` : finalMessage;
|
|
39057
|
+
displayStep(`git commit -m "${finalMessage}"${finalBody ? " (with body)" : ""}`);
|
|
39058
|
+
await execAsync6(`git commit -m ${JSON.stringify(fullMessage)}`);
|
|
39059
|
+
displaySuccess("Committed!");
|
|
39060
|
+
const { stdout: hash } = await execAsync6("git rev-parse --short HEAD");
|
|
39061
|
+
console.log(source_default.dim(` ${hash.trim()} ${finalMessage}
|
|
39062
|
+
`));
|
|
39063
|
+
await recordCommand("commit", [], Date.now() - start, true);
|
|
39064
|
+
committed = true;
|
|
39065
|
+
}
|
|
39066
|
+
}
|
|
39067
|
+
} catch (error3) {
|
|
39068
|
+
console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
|
|
39069
|
+
await recordCommand("commit", [], Date.now() - start, false);
|
|
39070
|
+
process.exit(1);
|
|
39071
|
+
}
|
|
39072
|
+
});
|
|
39073
|
+
}
|
|
39074
|
+
|
|
38740
39075
|
// src/lib/update-notifier.ts
|
|
38741
39076
|
import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
|
|
38742
39077
|
import { existsSync as existsSync6 } from "fs";
|
|
38743
39078
|
import { homedir as homedir2 } from "os";
|
|
38744
39079
|
import path5 from "path";
|
|
38745
39080
|
var PACKAGE_NAME2 = "hermes-git";
|
|
38746
|
-
var CHECK_INTERVAL =
|
|
39081
|
+
var CHECK_INTERVAL = 12 * 60 * 60 * 1000;
|
|
38747
39082
|
var CACHE_DIR = path5.join(homedir2(), ".hermes", "cache");
|
|
38748
39083
|
var CACHE_FILE = path5.join(CACHE_DIR, "update-check.json");
|
|
38749
|
-
async function
|
|
39084
|
+
async function fetchDistTags() {
|
|
38750
39085
|
try {
|
|
38751
|
-
const
|
|
38752
|
-
if (!
|
|
38753
|
-
return
|
|
38754
|
-
|
|
38755
|
-
return data.version || null;
|
|
39086
|
+
const res = await fetch(`https://registry.npmjs.org/-/package/${PACKAGE_NAME2}/dist-tags`, { signal: AbortSignal.timeout(5000) });
|
|
39087
|
+
if (!res.ok)
|
|
39088
|
+
return {};
|
|
39089
|
+
return await res.json();
|
|
38756
39090
|
} catch {
|
|
38757
|
-
return
|
|
39091
|
+
return {};
|
|
38758
39092
|
}
|
|
38759
39093
|
}
|
|
38760
39094
|
async function readCache() {
|
|
38761
39095
|
try {
|
|
38762
39096
|
if (!existsSync6(CACHE_FILE))
|
|
38763
39097
|
return null;
|
|
38764
|
-
|
|
38765
|
-
return JSON.parse(content);
|
|
39098
|
+
return JSON.parse(await readFile6(CACHE_FILE, "utf-8"));
|
|
38766
39099
|
} catch {
|
|
38767
39100
|
return null;
|
|
38768
39101
|
}
|
|
@@ -38773,36 +39106,66 @@ async function writeCache(cache) {
|
|
|
38773
39106
|
await writeFile6(CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
38774
39107
|
} catch {}
|
|
38775
39108
|
}
|
|
38776
|
-
function
|
|
38777
|
-
const
|
|
38778
|
-
const
|
|
39109
|
+
function compareVersions2(a, b) {
|
|
39110
|
+
const pa = a.split(".").map(Number);
|
|
39111
|
+
const pb = b.split(".").map(Number);
|
|
38779
39112
|
for (let i = 0;i < 3; i++) {
|
|
38780
|
-
if (
|
|
38781
|
-
return
|
|
38782
|
-
if (
|
|
38783
|
-
return
|
|
39113
|
+
if (pa[i] > pb[i])
|
|
39114
|
+
return 1;
|
|
39115
|
+
if (pa[i] < pb[i])
|
|
39116
|
+
return -1;
|
|
38784
39117
|
}
|
|
38785
|
-
return
|
|
39118
|
+
return 0;
|
|
39119
|
+
}
|
|
39120
|
+
async function enforceMinimumVersion(currentVersion) {
|
|
39121
|
+
try {
|
|
39122
|
+
const cache = await readCache();
|
|
39123
|
+
const now = Date.now();
|
|
39124
|
+
const stale = !cache || now - cache.lastChecked > CHECK_INTERVAL;
|
|
39125
|
+
let minimumVersion = cache?.minimumVersion ?? null;
|
|
39126
|
+
let latestVersion = cache?.latestVersion ?? null;
|
|
39127
|
+
if (stale) {
|
|
39128
|
+
const tags = await fetchDistTags();
|
|
39129
|
+
latestVersion = tags.latest ?? latestVersion;
|
|
39130
|
+
minimumVersion = tags.minimum ?? null;
|
|
39131
|
+
await writeCache({ lastChecked: now, latestVersion: latestVersion ?? undefined, minimumVersion: minimumVersion ?? undefined });
|
|
39132
|
+
}
|
|
39133
|
+
if (minimumVersion && compareVersions2(currentVersion, minimumVersion) < 0) {
|
|
39134
|
+
console.error("");
|
|
39135
|
+
console.error(source_default.red.bold(" ✖ This version of hermes is no longer supported."));
|
|
39136
|
+
console.error("");
|
|
39137
|
+
console.error(` You have ${source_default.dim(currentVersion)}, minimum required is ${source_default.red.bold(minimumVersion)}.`);
|
|
39138
|
+
console.error("");
|
|
39139
|
+
console.error(" Update now:");
|
|
39140
|
+
console.error(` ${source_default.cyan("npm install -g hermes-git@latest")}`);
|
|
39141
|
+
console.error(` ${source_default.dim("or: hermes update")}`);
|
|
39142
|
+
console.error("");
|
|
39143
|
+
process.exit(1);
|
|
39144
|
+
}
|
|
39145
|
+
} catch {}
|
|
38786
39146
|
}
|
|
38787
39147
|
async function checkForUpdates(currentVersion) {
|
|
38788
39148
|
try {
|
|
38789
39149
|
const cache = await readCache();
|
|
38790
39150
|
const now = Date.now();
|
|
38791
|
-
const
|
|
38792
|
-
let latestVersion = cache?.latestVersion
|
|
38793
|
-
if (
|
|
38794
|
-
|
|
39151
|
+
const stale = !cache || now - cache.lastChecked > CHECK_INTERVAL;
|
|
39152
|
+
let latestVersion = cache?.latestVersion ?? null;
|
|
39153
|
+
if (stale) {
|
|
39154
|
+
const tags = await fetchDistTags();
|
|
39155
|
+
latestVersion = tags.latest ?? null;
|
|
38795
39156
|
await writeCache({
|
|
38796
39157
|
lastChecked: now,
|
|
38797
|
-
latestVersion: latestVersion
|
|
39158
|
+
latestVersion: latestVersion ?? undefined,
|
|
39159
|
+
minimumVersion: cache?.minimumVersion
|
|
38798
39160
|
});
|
|
38799
39161
|
}
|
|
38800
|
-
if (latestVersion &&
|
|
39162
|
+
if (latestVersion && compareVersions2(latestVersion, currentVersion) > 0) {
|
|
39163
|
+
const gap = " ".repeat(Math.max(0, 17 - currentVersion.length - latestVersion.length));
|
|
38801
39164
|
console.log(source_default.yellow(`
|
|
38802
|
-
|
|
38803
|
-
console.log(source_default.yellow("│") +
|
|
38804
|
-
console.log(source_default.yellow("│") +
|
|
38805
|
-
console.log(source_default.yellow(
|
|
39165
|
+
┌──────────────────────────────────────────────────────┐`));
|
|
39166
|
+
console.log(source_default.yellow("│") + ` ${source_default.bold("Update available!")} ${source_default.dim(currentVersion)} → ${source_default.green.bold(latestVersion)}${gap}` + source_default.yellow("│"));
|
|
39167
|
+
console.log(source_default.yellow("│") + ` Run: ${source_default.cyan("hermes update")}` + " ".repeat(36) + source_default.yellow("│"));
|
|
39168
|
+
console.log(source_default.yellow(`└──────────────────────────────────────────────────────┘
|
|
38806
39169
|
`));
|
|
38807
39170
|
}
|
|
38808
39171
|
} catch {}
|
|
@@ -38889,7 +39252,7 @@ function printWorkflows() {
|
|
|
38889
39252
|
|
|
38890
39253
|
// src/index.ts
|
|
38891
39254
|
var program2 = new Command;
|
|
38892
|
-
var CURRENT_VERSION = "0.3.
|
|
39255
|
+
var CURRENT_VERSION = "0.3.5";
|
|
38893
39256
|
program2.name("hermes").description("Intent-driven Git, guided by AI").version(CURRENT_VERSION).action(() => {
|
|
38894
39257
|
printBanner(CURRENT_VERSION);
|
|
38895
39258
|
printWorkflows();
|
|
@@ -38907,5 +39270,5 @@ workflowCommand(program2);
|
|
|
38907
39270
|
configCommand(program2);
|
|
38908
39271
|
guardCommand(program2);
|
|
38909
39272
|
updateCommand(program2, CURRENT_VERSION);
|
|
38910
|
-
|
|
38911
|
-
program2.
|
|
39273
|
+
commitCommand(program2);
|
|
39274
|
+
enforceMinimumVersion(CURRENT_VERSION).then(() => program2.parseAsync()).then(() => checkForUpdates(CURRENT_VERSION)).catch(() => {});
|