hermes-git 0.3.0 → 0.3.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 +82 -24
- package/dist/index.js +712 -10
- 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
|
|
|
@@ -197,15 +258,17 @@ For each file: shows a proposed resolution, lets you accept, edit manually, or s
|
|
|
197
258
|
|
|
198
259
|
### `hermes workflow <name>`
|
|
199
260
|
|
|
200
|
-
One-command workflows for common patterns.
|
|
261
|
+
One-command workflows for common patterns. Available workflows are shown when you run `hermes` with no arguments.
|
|
201
262
|
|
|
202
263
|
```bash
|
|
203
264
|
hermes workflow pr-ready # fetch → rebase → push --force-with-lease
|
|
204
265
|
hermes workflow daily-sync # fetch all → show status → suggest next action
|
|
205
266
|
hermes workflow quick-commit # generate commit message from staged diff
|
|
206
|
-
hermes workflow list # show
|
|
267
|
+
hermes workflow list # show all workflows including project-specific
|
|
207
268
|
```
|
|
208
269
|
|
|
270
|
+
Define project-specific workflows in `.hermes/config.json` and they appear automatically in the help output.
|
|
271
|
+
|
|
209
272
|
---
|
|
210
273
|
|
|
211
274
|
### `hermes worktree new "<task>"`
|
|
@@ -224,8 +287,8 @@ hermes worktree new "fix memory leak"
|
|
|
224
287
|
Initialize project-level config (`.hermes/config.json`). Commit this to share branch patterns and workflows with your team.
|
|
225
288
|
|
|
226
289
|
```bash
|
|
227
|
-
hermes init #
|
|
228
|
-
hermes init --quick #
|
|
290
|
+
hermes init # interactive
|
|
291
|
+
hermes init --quick # use defaults
|
|
229
292
|
```
|
|
230
293
|
|
|
231
294
|
---
|
|
@@ -252,6 +315,8 @@ Hermes resolves config in this priority order:
|
|
|
252
315
|
| `.env` file in current dir | `ANTHROPIC_API_KEY=sk-ant-...` |
|
|
253
316
|
| `~/.config/hermes/config.json` | set via `hermes config set` |
|
|
254
317
|
|
|
318
|
+
Environment variables always win — useful for CI and Docker environments where you don't want a config file.
|
|
319
|
+
|
|
255
320
|
**Supported env vars:**
|
|
256
321
|
|
|
257
322
|
| Variable | Description |
|
|
@@ -277,9 +342,9 @@ If `HERMES_PROVIDER` is not set, Hermes auto-detects by using whichever key it f
|
|
|
277
342
|
|
|
278
343
|
1. **Reads your repo state** — branch, commits, dirty files, conflicts, remote tracking
|
|
279
344
|
2. **Sends context + intent to an AI** — using your configured provider
|
|
280
|
-
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
|
|
281
346
|
4. **Executes with display** — shows every command before running it
|
|
282
|
-
5. **You
|
|
347
|
+
5. **You stay in control** — interactive prompts for anything irreversible
|
|
283
348
|
|
|
284
349
|
---
|
|
285
350
|
|
|
@@ -301,30 +366,23 @@ If `HERMES_PROVIDER` is not set, Hermes auto-detects by using whichever key it f
|
|
|
301
366
|
hermes config setup
|
|
302
367
|
```
|
|
303
368
|
|
|
304
|
-
**Wrong provider
|
|
369
|
+
**Wrong provider being used**
|
|
305
370
|
|
|
306
371
|
```bash
|
|
307
372
|
hermes config set provider anthropic
|
|
308
|
-
hermes config list #
|
|
373
|
+
hermes config list # check sources — env vars override saved config
|
|
309
374
|
```
|
|
310
375
|
|
|
311
|
-
**Key
|
|
376
|
+
**Key set but not working**
|
|
312
377
|
|
|
313
378
|
```bash
|
|
314
|
-
#
|
|
315
|
-
hermes config list
|
|
316
|
-
|
|
317
|
-
# Environment variables override saved config
|
|
318
|
-
# Check for conflicting vars:
|
|
319
|
-
echo $ANTHROPIC_API_KEY
|
|
379
|
+
hermes config list # shows value and where it came from (env / .env / config)
|
|
320
380
|
```
|
|
321
381
|
|
|
322
|
-
**
|
|
382
|
+
**Update to latest**
|
|
323
383
|
|
|
324
384
|
```bash
|
|
325
|
-
hermes
|
|
326
|
-
hermes config list
|
|
327
|
-
git status
|
|
385
|
+
hermes update
|
|
328
386
|
```
|
|
329
387
|
|
|
330
388
|
---
|
package/dist/index.js
CHANGED
|
@@ -38120,18 +38120,635 @@ Valid keys:`));
|
|
|
38120
38120
|
}
|
|
38121
38121
|
}
|
|
38122
38122
|
|
|
38123
|
-
// src/
|
|
38124
|
-
import { readFile as readFile5,
|
|
38123
|
+
// src/commands/guard.ts
|
|
38124
|
+
import { writeFile as writeFile5, readFile as readFile5, chmod as chmod2, mkdir as mkdir4 } from "fs/promises";
|
|
38125
38125
|
import { existsSync as existsSync5 } from "fs";
|
|
38126
|
+
import { exec as exec4 } from "child_process";
|
|
38127
|
+
import { promisify as promisify4 } from "util";
|
|
38128
|
+
|
|
38129
|
+
// src/lib/secrets.ts
|
|
38130
|
+
import { exec as exec3 } from "child_process";
|
|
38131
|
+
import { promisify as promisify3 } from "util";
|
|
38132
|
+
var execAsync3 = promisify3(exec3);
|
|
38133
|
+
var BLOCKED_FILENAMES = [
|
|
38134
|
+
{ pattern: /^\.env$/, description: "Environment variable file" },
|
|
38135
|
+
{ pattern: /^\.env\..+/, description: "Environment variable file" },
|
|
38136
|
+
{ pattern: /id_rsa$/, description: "SSH private key" },
|
|
38137
|
+
{ pattern: /id_ed25519$/, description: "SSH private key" },
|
|
38138
|
+
{ pattern: /id_ecdsa$/, description: "SSH private key" },
|
|
38139
|
+
{ pattern: /id_dsa$/, description: "SSH private key" },
|
|
38140
|
+
{ pattern: /\.pem$/, description: "PEM certificate/key file" },
|
|
38141
|
+
{ pattern: /\.p12$/, description: "PKCS#12 certificate file" },
|
|
38142
|
+
{ pattern: /\.pfx$/, description: "PKCS#12 certificate file" },
|
|
38143
|
+
{ pattern: /\.jks$/, description: "Java keystore file" },
|
|
38144
|
+
{ pattern: /\.keystore$/, description: "Keystore file" },
|
|
38145
|
+
{ pattern: /credentials\.json$/, description: "Credentials file" },
|
|
38146
|
+
{ pattern: /google-services\.json$/, description: "Google services config (may contain API keys)" },
|
|
38147
|
+
{ pattern: /firebase-adminsdk.*\.json$/, description: "Firebase admin SDK credentials" },
|
|
38148
|
+
{ pattern: /serviceAccountKey.*\.json$/, description: "Service account credentials" },
|
|
38149
|
+
{ pattern: /\.netrc$/, description: ".netrc credentials file" }
|
|
38150
|
+
];
|
|
38151
|
+
var WARNED_FILENAMES = [
|
|
38152
|
+
{ pattern: /\.npmrc$/, description: ".npmrc (may contain auth tokens)" },
|
|
38153
|
+
{ pattern: /\.pypirc$/, description: ".pypirc (may contain PyPI token)" },
|
|
38154
|
+
{ pattern: /secrets?\.(json|yaml|yml|toml)$/, description: "Secrets config file" },
|
|
38155
|
+
{ pattern: /config\.(local|private|secret)\.(json|yaml|yml|toml|js|ts)$/, description: "Local/private config file" },
|
|
38156
|
+
{ pattern: /private.*\.(json|yaml|yml|toml)$/, description: "Private config file" }
|
|
38157
|
+
];
|
|
38158
|
+
var SECRET_PATTERNS = [
|
|
38159
|
+
{
|
|
38160
|
+
id: "private-key",
|
|
38161
|
+
description: "Private key header",
|
|
38162
|
+
severity: "blocked",
|
|
38163
|
+
pattern: /-----BEGIN\s+(RSA\s+|EC\s+|OPENSSH\s+|DSA\s+)?PRIVATE KEY-----/
|
|
38164
|
+
},
|
|
38165
|
+
{
|
|
38166
|
+
id: "anthropic-key",
|
|
38167
|
+
description: "Anthropic API key",
|
|
38168
|
+
severity: "blocked",
|
|
38169
|
+
pattern: /sk-ant-[a-zA-Z0-9_-]{80,}/,
|
|
38170
|
+
rotation: "https://console.anthropic.com/settings/keys"
|
|
38171
|
+
},
|
|
38172
|
+
{
|
|
38173
|
+
id: "openai-key",
|
|
38174
|
+
description: "OpenAI API key",
|
|
38175
|
+
severity: "blocked",
|
|
38176
|
+
pattern: /sk-(?:proj-)?[a-zA-Z0-9]{40,}/,
|
|
38177
|
+
rotation: "https://platform.openai.com/api-keys"
|
|
38178
|
+
},
|
|
38179
|
+
{
|
|
38180
|
+
id: "google-api-key",
|
|
38181
|
+
description: "Google API key",
|
|
38182
|
+
severity: "blocked",
|
|
38183
|
+
pattern: /AIza[a-zA-Z0-9_-]{35}/,
|
|
38184
|
+
rotation: "https://console.cloud.google.com/apis/credentials"
|
|
38185
|
+
},
|
|
38186
|
+
{
|
|
38187
|
+
id: "aws-access-key",
|
|
38188
|
+
description: "AWS access key ID",
|
|
38189
|
+
severity: "blocked",
|
|
38190
|
+
pattern: /AKIA[A-Z0-9]{16}/,
|
|
38191
|
+
rotation: "https://console.aws.amazon.com/iam/home#/security_credentials"
|
|
38192
|
+
},
|
|
38193
|
+
{
|
|
38194
|
+
id: "aws-secret-key",
|
|
38195
|
+
description: "AWS secret access key",
|
|
38196
|
+
severity: "blocked",
|
|
38197
|
+
pattern: /aws[_-]?secret[_-]?access[_-]?key\s*[=:]\s*["']?[a-zA-Z0-9/+]{40}/i,
|
|
38198
|
+
rotation: "https://console.aws.amazon.com/iam/home#/security_credentials"
|
|
38199
|
+
},
|
|
38200
|
+
{
|
|
38201
|
+
id: "github-token",
|
|
38202
|
+
description: "GitHub personal access token",
|
|
38203
|
+
severity: "blocked",
|
|
38204
|
+
pattern: /gh[pousr]_[a-zA-Z0-9]{36}/,
|
|
38205
|
+
rotation: "https://github.com/settings/tokens"
|
|
38206
|
+
},
|
|
38207
|
+
{
|
|
38208
|
+
id: "github-pat-v2",
|
|
38209
|
+
description: "GitHub fine-grained personal access token",
|
|
38210
|
+
severity: "blocked",
|
|
38211
|
+
pattern: /github_pat_[a-zA-Z0-9_]{82}/,
|
|
38212
|
+
rotation: "https://github.com/settings/tokens"
|
|
38213
|
+
},
|
|
38214
|
+
{
|
|
38215
|
+
id: "stripe-key",
|
|
38216
|
+
description: "Stripe secret key",
|
|
38217
|
+
severity: "blocked",
|
|
38218
|
+
pattern: /(?:sk|rk)_live_[a-zA-Z0-9]{24,}/,
|
|
38219
|
+
rotation: "https://dashboard.stripe.com/apikeys"
|
|
38220
|
+
},
|
|
38221
|
+
{
|
|
38222
|
+
id: "sendgrid-key",
|
|
38223
|
+
description: "SendGrid API key",
|
|
38224
|
+
severity: "blocked",
|
|
38225
|
+
pattern: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/,
|
|
38226
|
+
rotation: "https://app.sendgrid.com/settings/api_keys"
|
|
38227
|
+
},
|
|
38228
|
+
{
|
|
38229
|
+
id: "twilio-key",
|
|
38230
|
+
description: "Twilio API key",
|
|
38231
|
+
severity: "blocked",
|
|
38232
|
+
pattern: /SK[a-f0-9]{32}/,
|
|
38233
|
+
rotation: "https://www.twilio.com/console/project/api-keys"
|
|
38234
|
+
},
|
|
38235
|
+
{
|
|
38236
|
+
id: "db-url",
|
|
38237
|
+
description: "Database URL with credentials",
|
|
38238
|
+
severity: "blocked",
|
|
38239
|
+
pattern: /(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|redis):\/\/[^:/?#\s]+:[^@\s]+@/
|
|
38240
|
+
},
|
|
38241
|
+
{
|
|
38242
|
+
id: "password-assignment",
|
|
38243
|
+
description: "Hardcoded password",
|
|
38244
|
+
severity: "warn",
|
|
38245
|
+
pattern: /(?:password|passwd|pwd)\s*[=:]\s*["'][^"']{6,}["']/i
|
|
38246
|
+
},
|
|
38247
|
+
{
|
|
38248
|
+
id: "secret-assignment",
|
|
38249
|
+
description: "Hardcoded secret",
|
|
38250
|
+
severity: "warn",
|
|
38251
|
+
pattern: /(?:secret|api_secret|client_secret)\s*[=:]\s*["'][^"']{8,}["']/i
|
|
38252
|
+
},
|
|
38253
|
+
{
|
|
38254
|
+
id: "token-assignment",
|
|
38255
|
+
description: "Hardcoded token",
|
|
38256
|
+
severity: "warn",
|
|
38257
|
+
pattern: /(?<![a-z])token\s*[=:]\s*["'][a-zA-Z0-9_\-\.]{16,}["']/i
|
|
38258
|
+
}
|
|
38259
|
+
];
|
|
38260
|
+
async function getStagedFiles() {
|
|
38261
|
+
const { stdout } = await execAsync3("git diff --cached --name-only --diff-filter=ACMR");
|
|
38262
|
+
return stdout.trim().split(`
|
|
38263
|
+
`).filter(Boolean);
|
|
38264
|
+
}
|
|
38265
|
+
async function readStagedFile(file) {
|
|
38266
|
+
try {
|
|
38267
|
+
const { stdout } = await execAsync3(`git show ":${file}"`, {
|
|
38268
|
+
maxBuffer: 2 * 1024 * 1024
|
|
38269
|
+
});
|
|
38270
|
+
return stdout;
|
|
38271
|
+
} catch {
|
|
38272
|
+
return null;
|
|
38273
|
+
}
|
|
38274
|
+
}
|
|
38275
|
+
function checkFilename(file) {
|
|
38276
|
+
const basename = file.split("/").pop() || file;
|
|
38277
|
+
for (const { pattern, description } of BLOCKED_FILENAMES) {
|
|
38278
|
+
if (pattern.test(basename)) {
|
|
38279
|
+
return {
|
|
38280
|
+
patternId: "filename-blocked",
|
|
38281
|
+
description: `Sensitive filename: ${description}`,
|
|
38282
|
+
severity: "blocked",
|
|
38283
|
+
line: 0,
|
|
38284
|
+
preview: file
|
|
38285
|
+
};
|
|
38286
|
+
}
|
|
38287
|
+
}
|
|
38288
|
+
for (const { pattern, description } of WARNED_FILENAMES) {
|
|
38289
|
+
if (pattern.test(basename)) {
|
|
38290
|
+
return {
|
|
38291
|
+
patternId: "filename-warned",
|
|
38292
|
+
description: `Potentially sensitive filename: ${description}`,
|
|
38293
|
+
severity: "warn",
|
|
38294
|
+
line: 0,
|
|
38295
|
+
preview: file
|
|
38296
|
+
};
|
|
38297
|
+
}
|
|
38298
|
+
}
|
|
38299
|
+
return null;
|
|
38300
|
+
}
|
|
38301
|
+
function scanContent(content) {
|
|
38302
|
+
const lines2 = content.split(`
|
|
38303
|
+
`);
|
|
38304
|
+
const findings = [];
|
|
38305
|
+
const seen = new Set;
|
|
38306
|
+
for (let i = 0;i < lines2.length; i++) {
|
|
38307
|
+
const line = lines2[i];
|
|
38308
|
+
for (const sp of SECRET_PATTERNS) {
|
|
38309
|
+
if (sp.pattern.test(line)) {
|
|
38310
|
+
const dedupeKey = `${sp.id}:${i}`;
|
|
38311
|
+
if (seen.has(dedupeKey))
|
|
38312
|
+
continue;
|
|
38313
|
+
seen.add(dedupeKey);
|
|
38314
|
+
findings.push({
|
|
38315
|
+
patternId: sp.id,
|
|
38316
|
+
description: sp.description,
|
|
38317
|
+
severity: sp.severity,
|
|
38318
|
+
line: i + 1,
|
|
38319
|
+
preview: redactLine(line, sp.pattern),
|
|
38320
|
+
rotation: sp.rotation
|
|
38321
|
+
});
|
|
38322
|
+
}
|
|
38323
|
+
}
|
|
38324
|
+
}
|
|
38325
|
+
return findings;
|
|
38326
|
+
}
|
|
38327
|
+
function redactLine(line, pattern) {
|
|
38328
|
+
const redacted = line.replace(pattern, (match) => {
|
|
38329
|
+
if (match.length <= 8)
|
|
38330
|
+
return "****";
|
|
38331
|
+
return match.slice(0, 4) + "..." + match.slice(-4).replace(/./g, "*");
|
|
38332
|
+
});
|
|
38333
|
+
return redacted.length > 120 ? redacted.slice(0, 117) + "..." : redacted;
|
|
38334
|
+
}
|
|
38335
|
+
async function scanStagedFiles() {
|
|
38336
|
+
const files = await getStagedFiles();
|
|
38337
|
+
const result = { blocked: [], warned: [], clean: [], skipped: [] };
|
|
38338
|
+
await Promise.all(files.map(async (file) => {
|
|
38339
|
+
const allFindings = [];
|
|
38340
|
+
const filenameFindig = checkFilename(file);
|
|
38341
|
+
if (filenameFindig)
|
|
38342
|
+
allFindings.push(filenameFindig);
|
|
38343
|
+
const content = await readStagedFile(file);
|
|
38344
|
+
if (content === null) {
|
|
38345
|
+
result.skipped.push(file);
|
|
38346
|
+
return;
|
|
38347
|
+
}
|
|
38348
|
+
allFindings.push(...scanContent(content));
|
|
38349
|
+
if (allFindings.length === 0) {
|
|
38350
|
+
result.clean.push(file);
|
|
38351
|
+
return;
|
|
38352
|
+
}
|
|
38353
|
+
const maxSeverity = allFindings.some((f) => f.severity === "blocked") ? "blocked" : "warn";
|
|
38354
|
+
const fileFindings = { file, findings: allFindings, maxSeverity };
|
|
38355
|
+
if (maxSeverity === "blocked") {
|
|
38356
|
+
result.blocked.push(fileFindings);
|
|
38357
|
+
} else {
|
|
38358
|
+
result.warned.push(fileFindings);
|
|
38359
|
+
}
|
|
38360
|
+
}));
|
|
38361
|
+
return result;
|
|
38362
|
+
}
|
|
38363
|
+
|
|
38364
|
+
// src/commands/guard.ts
|
|
38365
|
+
var execAsync4 = promisify4(exec4);
|
|
38366
|
+
function guardCommand(program2) {
|
|
38367
|
+
const guard = program2.command("guard").description("Scan staged files for secrets and sensitive content before committing");
|
|
38368
|
+
guard.option("--hook", "Run in non-interactive hook mode (exit 1 on blockers)").action(async (options) => {
|
|
38369
|
+
await runScan(options.hook ?? false);
|
|
38370
|
+
});
|
|
38371
|
+
guard.command("install-hook").description("Install hermes guard as a git pre-commit hook").action(async () => {
|
|
38372
|
+
try {
|
|
38373
|
+
const gitDir = await execAsync4("git rev-parse --git-dir").then((r) => r.stdout.trim()).catch(() => null);
|
|
38374
|
+
if (!gitDir) {
|
|
38375
|
+
console.error(source_default.red("❌ Not a git repository"));
|
|
38376
|
+
process.exit(1);
|
|
38377
|
+
}
|
|
38378
|
+
const hooksDir = `${gitDir}/hooks`;
|
|
38379
|
+
await mkdir4(hooksDir, { recursive: true });
|
|
38380
|
+
const hookPath = `${hooksDir}/pre-commit`;
|
|
38381
|
+
const hookScript = [
|
|
38382
|
+
"#!/bin/sh",
|
|
38383
|
+
"# Installed by hermes guard install-hook",
|
|
38384
|
+
"hermes guard --hook"
|
|
38385
|
+
].join(`
|
|
38386
|
+
`) + `
|
|
38387
|
+
`;
|
|
38388
|
+
if (existsSync5(hookPath)) {
|
|
38389
|
+
const existing = await readFile5(hookPath, "utf-8");
|
|
38390
|
+
if (existing.includes("hermes guard")) {
|
|
38391
|
+
console.log(source_default.dim("Hook already installed."));
|
|
38392
|
+
return;
|
|
38393
|
+
}
|
|
38394
|
+
await writeFile5(hookPath, existing.trimEnd() + `
|
|
38395
|
+
|
|
38396
|
+
` + hookScript.split(`
|
|
38397
|
+
`).slice(1).join(`
|
|
38398
|
+
`));
|
|
38399
|
+
} else {
|
|
38400
|
+
await writeFile5(hookPath, hookScript);
|
|
38401
|
+
}
|
|
38402
|
+
await chmod2(hookPath, 493);
|
|
38403
|
+
console.log(`${source_default.green("✓")} Pre-commit hook installed at ${source_default.dim(hookPath)}`);
|
|
38404
|
+
console.log(source_default.dim(" hermes guard will run automatically before every commit."));
|
|
38405
|
+
} catch (error3) {
|
|
38406
|
+
console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
|
|
38407
|
+
process.exit(1);
|
|
38408
|
+
}
|
|
38409
|
+
});
|
|
38410
|
+
guard.command("uninstall-hook").description("Remove hermes guard from the git pre-commit hook").action(async () => {
|
|
38411
|
+
try {
|
|
38412
|
+
const gitDir = await execAsync4("git rev-parse --git-dir").then((r) => r.stdout.trim()).catch(() => null);
|
|
38413
|
+
if (!gitDir) {
|
|
38414
|
+
console.error(source_default.red("❌ Not a git repository"));
|
|
38415
|
+
process.exit(1);
|
|
38416
|
+
}
|
|
38417
|
+
const hookPath = `${gitDir}/hooks/pre-commit`;
|
|
38418
|
+
if (!existsSync5(hookPath)) {
|
|
38419
|
+
console.log(source_default.dim("No pre-commit hook found."));
|
|
38420
|
+
return;
|
|
38421
|
+
}
|
|
38422
|
+
const content = await readFile5(hookPath, "utf-8");
|
|
38423
|
+
if (!content.includes("hermes guard")) {
|
|
38424
|
+
console.log(source_default.dim("hermes guard is not in the pre-commit hook."));
|
|
38425
|
+
return;
|
|
38426
|
+
}
|
|
38427
|
+
const cleaned = content.split(`
|
|
38428
|
+
`).filter((line) => !line.includes("hermes guard") && line !== "# Installed by hermes guard install-hook").join(`
|
|
38429
|
+
`).replace(/\n{3,}/g, `
|
|
38430
|
+
|
|
38431
|
+
`).trim() + `
|
|
38432
|
+
`;
|
|
38433
|
+
if (cleaned === `#!/bin/sh
|
|
38434
|
+
`) {
|
|
38435
|
+
const { unlink } = await import("fs/promises");
|
|
38436
|
+
await unlink(hookPath);
|
|
38437
|
+
console.log(`${source_default.green("✓")} Pre-commit hook removed.`);
|
|
38438
|
+
} else {
|
|
38439
|
+
await writeFile5(hookPath, cleaned);
|
|
38440
|
+
console.log(`${source_default.green("✓")} hermes guard removed from pre-commit hook.`);
|
|
38441
|
+
}
|
|
38442
|
+
} catch (error3) {
|
|
38443
|
+
console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
|
|
38444
|
+
process.exit(1);
|
|
38445
|
+
}
|
|
38446
|
+
});
|
|
38447
|
+
}
|
|
38448
|
+
async function runScan(hookMode) {
|
|
38449
|
+
try {
|
|
38450
|
+
const staged = await getStagedFiles();
|
|
38451
|
+
if (staged.length === 0) {
|
|
38452
|
+
if (!hookMode)
|
|
38453
|
+
console.log(source_default.dim("No staged files to scan."));
|
|
38454
|
+
return;
|
|
38455
|
+
}
|
|
38456
|
+
if (!hookMode) {
|
|
38457
|
+
console.log(source_default.bold(`
|
|
38458
|
+
Scanning ${staged.length} staged file${staged.length === 1 ? "" : "s"}...
|
|
38459
|
+
`));
|
|
38460
|
+
}
|
|
38461
|
+
const result = await scanStagedFiles();
|
|
38462
|
+
const hasBlockers = result.blocked.length > 0;
|
|
38463
|
+
const hasWarnings = result.warned.length > 0;
|
|
38464
|
+
if (!hasBlockers && !hasWarnings) {
|
|
38465
|
+
if (!hookMode) {
|
|
38466
|
+
console.log(source_default.green(" ✓ All clear") + source_default.dim(` — ${staged.length} file${staged.length === 1 ? "" : "s"} scanned, nothing suspicious found.
|
|
38467
|
+
`));
|
|
38468
|
+
}
|
|
38469
|
+
return;
|
|
38470
|
+
}
|
|
38471
|
+
if (!hookMode) {
|
|
38472
|
+
printReport(result);
|
|
38473
|
+
} else {
|
|
38474
|
+
if (hasBlockers) {
|
|
38475
|
+
process.stderr.write(source_default.red(`
|
|
38476
|
+
hermes guard: ${result.blocked.length} blocked file${result.blocked.length === 1 ? "" : "s"} with secrets detected.
|
|
38477
|
+
`));
|
|
38478
|
+
for (const f of result.blocked) {
|
|
38479
|
+
process.stderr.write(source_default.dim(` ${f.file}: ${f.findings.map((x) => x.description).join(", ")}
|
|
38480
|
+
`));
|
|
38481
|
+
}
|
|
38482
|
+
process.stderr.write(source_default.dim(`
|
|
38483
|
+
Run \`hermes guard\` for details and remediation advice.
|
|
38484
|
+
|
|
38485
|
+
`));
|
|
38486
|
+
}
|
|
38487
|
+
}
|
|
38488
|
+
if (!hookMode && (hasBlockers || hasWarnings)) {
|
|
38489
|
+
await printAIExplanation(result);
|
|
38490
|
+
if (hasBlockers) {
|
|
38491
|
+
const { action } = await esm_default12.prompt([
|
|
38492
|
+
{
|
|
38493
|
+
type: "list",
|
|
38494
|
+
name: "action",
|
|
38495
|
+
message: source_default.red("Blocked secrets found. What do you want to do?"),
|
|
38496
|
+
choices: [
|
|
38497
|
+
{ name: "Abort — I will fix these before committing", value: "abort" },
|
|
38498
|
+
{ name: "Unstage the flagged files and continue", value: "unstage" },
|
|
38499
|
+
{ name: "Proceed anyway (I know what I'm doing)", value: "proceed" }
|
|
38500
|
+
],
|
|
38501
|
+
default: "abort"
|
|
38502
|
+
}
|
|
38503
|
+
]);
|
|
38504
|
+
if (action === "abort") {
|
|
38505
|
+
console.log(source_default.dim(`
|
|
38506
|
+
Commit aborted. Fix the issues above, then try again.
|
|
38507
|
+
`));
|
|
38508
|
+
process.exit(1);
|
|
38509
|
+
}
|
|
38510
|
+
if (action === "unstage") {
|
|
38511
|
+
for (const f of result.blocked) {
|
|
38512
|
+
await execAsync4(`git restore --staged "${f.file}"`);
|
|
38513
|
+
console.log(source_default.dim(` Unstaged: ${f.file}`));
|
|
38514
|
+
}
|
|
38515
|
+
console.log(source_default.yellow(`
|
|
38516
|
+
Flagged files have been unstaged. Commit the rest? (run git commit again)`));
|
|
38517
|
+
process.exit(0);
|
|
38518
|
+
}
|
|
38519
|
+
console.log(source_default.dim(`
|
|
38520
|
+
Proceeding. Be careful.
|
|
38521
|
+
`));
|
|
38522
|
+
} else if (hasWarnings) {
|
|
38523
|
+
const { proceed } = await esm_default12.prompt([
|
|
38524
|
+
{
|
|
38525
|
+
type: "confirm",
|
|
38526
|
+
name: "proceed",
|
|
38527
|
+
message: source_default.yellow("Warnings found. Proceed with commit?"),
|
|
38528
|
+
default: true
|
|
38529
|
+
}
|
|
38530
|
+
]);
|
|
38531
|
+
if (!proceed) {
|
|
38532
|
+
console.log(source_default.dim(`
|
|
38533
|
+
Commit aborted.
|
|
38534
|
+
`));
|
|
38535
|
+
process.exit(1);
|
|
38536
|
+
}
|
|
38537
|
+
}
|
|
38538
|
+
}
|
|
38539
|
+
if (hookMode && hasBlockers) {
|
|
38540
|
+
process.exit(1);
|
|
38541
|
+
}
|
|
38542
|
+
} catch (error3) {
|
|
38543
|
+
if (!hookMode) {
|
|
38544
|
+
console.error(source_default.dim(" guard scan failed:"), error3 instanceof Error ? error3.message : error3);
|
|
38545
|
+
}
|
|
38546
|
+
process.exit(0);
|
|
38547
|
+
}
|
|
38548
|
+
}
|
|
38549
|
+
function printReport(result) {
|
|
38550
|
+
const totalFiles = result.blocked.length + result.warned.length + result.clean.length + result.skipped.length;
|
|
38551
|
+
if (result.blocked.length > 0) {
|
|
38552
|
+
console.log(source_default.red.bold(" ✖ Secrets detected") + source_default.dim(` — review before committing
|
|
38553
|
+
`));
|
|
38554
|
+
} else {
|
|
38555
|
+
console.log(source_default.yellow.bold(" ⚠ Warnings") + source_default.dim(` — review before committing
|
|
38556
|
+
`));
|
|
38557
|
+
}
|
|
38558
|
+
for (const ff of result.blocked) {
|
|
38559
|
+
console.log(source_default.red(` BLOCKED `) + source_default.bold(ff.file));
|
|
38560
|
+
for (const f of ff.findings) {
|
|
38561
|
+
const lineRef = f.line > 0 ? source_default.dim(` line ${f.line}`) : "";
|
|
38562
|
+
console.log(source_default.dim(" ") + source_default.red("●") + " " + f.description + lineRef);
|
|
38563
|
+
if (f.preview && f.line > 0) {
|
|
38564
|
+
console.log(source_default.dim(" " + f.preview.trim()));
|
|
38565
|
+
}
|
|
38566
|
+
if (f.rotation) {
|
|
38567
|
+
console.log(source_default.dim(` Rotate at: ${f.rotation}`));
|
|
38568
|
+
}
|
|
38569
|
+
}
|
|
38570
|
+
console.log();
|
|
38571
|
+
}
|
|
38572
|
+
for (const ff of result.warned) {
|
|
38573
|
+
console.log(source_default.yellow(` WARN `) + source_default.bold(ff.file));
|
|
38574
|
+
for (const f of ff.findings) {
|
|
38575
|
+
const lineRef = f.line > 0 ? source_default.dim(` line ${f.line}`) : "";
|
|
38576
|
+
console.log(source_default.dim(" ") + source_default.yellow("●") + " " + f.description + lineRef);
|
|
38577
|
+
if (f.preview && f.line > 0) {
|
|
38578
|
+
console.log(source_default.dim(" " + f.preview.trim()));
|
|
38579
|
+
}
|
|
38580
|
+
}
|
|
38581
|
+
console.log();
|
|
38582
|
+
}
|
|
38583
|
+
if (result.clean.length > 0) {
|
|
38584
|
+
console.log(source_default.dim(` ✓ ${result.clean.length} file${result.clean.length === 1 ? "" : "s"} clean`));
|
|
38585
|
+
}
|
|
38586
|
+
if (result.skipped.length > 0) {
|
|
38587
|
+
console.log(source_default.dim(` − ${result.skipped.length} binary/unreadable file${result.skipped.length === 1 ? "" : "s"} skipped`));
|
|
38588
|
+
}
|
|
38589
|
+
console.log();
|
|
38590
|
+
}
|
|
38591
|
+
async function printAIExplanation(result) {
|
|
38592
|
+
const allFindings = [...result.blocked, ...result.warned];
|
|
38593
|
+
if (allFindings.length === 0)
|
|
38594
|
+
return;
|
|
38595
|
+
const findingSummary = allFindings.map((ff) => {
|
|
38596
|
+
const items = ff.findings.map((f) => ` - ${f.description}${f.line > 0 ? ` (line ${f.line})` : ""}`).join(`
|
|
38597
|
+
`);
|
|
38598
|
+
return `File: ${ff.file}
|
|
38599
|
+
Severity: ${ff.maxSeverity}
|
|
38600
|
+
${items}`;
|
|
38601
|
+
}).join(`
|
|
38602
|
+
|
|
38603
|
+
`);
|
|
38604
|
+
process.stdout.write(source_default.dim(" Asking AI for context...\r"));
|
|
38605
|
+
try {
|
|
38606
|
+
const explanation = await getAISuggestion(`A developer is about to commit these files that were flagged by a secret scanner:
|
|
38607
|
+
|
|
38608
|
+
${findingSummary}
|
|
38609
|
+
|
|
38610
|
+
For each finding, briefly explain:
|
|
38611
|
+
1. The specific risk if this gets committed (e.g., "Anyone with repo access can use this key to...")
|
|
38612
|
+
2. One concrete remediation step (e.g., "Add .env to .gitignore and use process.env instead")
|
|
38613
|
+
|
|
38614
|
+
Be direct and specific. 3-4 sentences per finding max. No preamble.`);
|
|
38615
|
+
process.stdout.write(" \r");
|
|
38616
|
+
console.log(source_default.bold(` What this means:
|
|
38617
|
+
`));
|
|
38618
|
+
const indented = explanation.split(`
|
|
38619
|
+
`).map((l) => " " + l).join(`
|
|
38620
|
+
`);
|
|
38621
|
+
console.log(indented);
|
|
38622
|
+
console.log();
|
|
38623
|
+
} catch {
|
|
38624
|
+
process.stdout.write(" \r");
|
|
38625
|
+
}
|
|
38626
|
+
}
|
|
38627
|
+
|
|
38628
|
+
// src/commands/update.ts
|
|
38629
|
+
import { exec as exec5 } from "child_process";
|
|
38630
|
+
import { promisify as promisify5 } from "util";
|
|
38631
|
+
var execAsync5 = promisify5(exec5);
|
|
38632
|
+
var PACKAGE_NAME = "hermes-git";
|
|
38633
|
+
function updateCommand(program2, currentVersion) {
|
|
38634
|
+
program2.command("update").description("Update hermes to the latest version").option("--check", "Check for updates without installing").option("--bun", "Use bun instead of npm to install").action(async (options) => {
|
|
38635
|
+
try {
|
|
38636
|
+
process.stdout.write(source_default.dim(" Checking npm for latest version..."));
|
|
38637
|
+
const latest = await fetchLatestVersion();
|
|
38638
|
+
process.stdout.write("\r" + " ".repeat(50) + "\r");
|
|
38639
|
+
if (!latest) {
|
|
38640
|
+
console.error(source_default.red(" ✖ Could not reach npm registry. Check your connection."));
|
|
38641
|
+
process.exit(1);
|
|
38642
|
+
}
|
|
38643
|
+
const isNewer = compareVersions(latest, currentVersion) > 0;
|
|
38644
|
+
const isAhead = compareVersions(currentVersion, latest) > 0;
|
|
38645
|
+
if (!isNewer) {
|
|
38646
|
+
if (isAhead) {
|
|
38647
|
+
console.log(source_default.green(" ✓ ") + `You're running ${source_default.cyan(`v${currentVersion}`)} ` + source_default.dim(`(latest on npm is ${latest})`));
|
|
38648
|
+
} else {
|
|
38649
|
+
console.log(source_default.green(` ✓ Already on the latest version`) + source_default.dim(` (v${currentVersion})`));
|
|
38650
|
+
}
|
|
38651
|
+
return;
|
|
38652
|
+
}
|
|
38653
|
+
console.log(` Update available ${source_default.dim(currentVersion)} ${source_default.dim("→")} ${source_default.green.bold(latest)}
|
|
38654
|
+
`);
|
|
38655
|
+
if (options.check)
|
|
38656
|
+
return;
|
|
38657
|
+
const pm = options.bun ? "bun" : await detectPackageManager();
|
|
38658
|
+
const installCmd = buildInstallCommand(pm);
|
|
38659
|
+
console.log(` Running: ${source_default.cyan(installCmd)}
|
|
38660
|
+
`);
|
|
38661
|
+
const { stdout, stderr } = await execAsync5(installCmd, { timeout: 120000 });
|
|
38662
|
+
const output = (stdout + stderr).trim();
|
|
38663
|
+
if (output) {
|
|
38664
|
+
output.split(`
|
|
38665
|
+
`).forEach((line) => {
|
|
38666
|
+
console.log(source_default.dim(" " + line));
|
|
38667
|
+
});
|
|
38668
|
+
console.log();
|
|
38669
|
+
}
|
|
38670
|
+
console.log(source_default.green(" ✓ Updated to ") + source_default.bold(`v${latest}`) + source_default.dim(" Restart your terminal if the version does not change."));
|
|
38671
|
+
} catch (error3) {
|
|
38672
|
+
console.error(source_default.red(`
|
|
38673
|
+
✖ Update failed`));
|
|
38674
|
+
console.error(source_default.dim(" " + (error3.message || error3)));
|
|
38675
|
+
console.log(source_default.dim(`
|
|
38676
|
+
Try manually: npm install -g ${PACKAGE_NAME}@latest`));
|
|
38677
|
+
process.exit(1);
|
|
38678
|
+
}
|
|
38679
|
+
});
|
|
38680
|
+
}
|
|
38681
|
+
async function fetchLatestVersion() {
|
|
38682
|
+
try {
|
|
38683
|
+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`);
|
|
38684
|
+
if (!res.ok)
|
|
38685
|
+
return null;
|
|
38686
|
+
const data = await res.json();
|
|
38687
|
+
return data.version ?? null;
|
|
38688
|
+
} catch {
|
|
38689
|
+
return null;
|
|
38690
|
+
}
|
|
38691
|
+
}
|
|
38692
|
+
function compareVersions(a, b) {
|
|
38693
|
+
const pa = a.split(".").map(Number);
|
|
38694
|
+
const pb = b.split(".").map(Number);
|
|
38695
|
+
for (let i = 0;i < 3; i++) {
|
|
38696
|
+
if (pa[i] > pb[i])
|
|
38697
|
+
return 1;
|
|
38698
|
+
if (pa[i] < pb[i])
|
|
38699
|
+
return -1;
|
|
38700
|
+
}
|
|
38701
|
+
return 0;
|
|
38702
|
+
}
|
|
38703
|
+
async function detectPackageManager() {
|
|
38704
|
+
if (process.execPath?.includes("bun") || process.argv[0]?.includes("bun")) {
|
|
38705
|
+
return "bun";
|
|
38706
|
+
}
|
|
38707
|
+
try {
|
|
38708
|
+
const { stdout } = await execAsync5("bun --version", { timeout: 3000 });
|
|
38709
|
+
if (stdout.trim()) {
|
|
38710
|
+
try {
|
|
38711
|
+
const { stdout: list } = await execAsync5("bun pm ls -g 2>/dev/null", { timeout: 3000 });
|
|
38712
|
+
if (list.includes(PACKAGE_NAME))
|
|
38713
|
+
return "bun";
|
|
38714
|
+
} catch {}
|
|
38715
|
+
}
|
|
38716
|
+
} catch {}
|
|
38717
|
+
try {
|
|
38718
|
+
const { stdout } = await execAsync5("pnpm --version", { timeout: 3000 });
|
|
38719
|
+
if (stdout.trim()) {
|
|
38720
|
+
try {
|
|
38721
|
+
const { stdout: list } = await execAsync5("pnpm list -g --depth=0 2>/dev/null", { timeout: 3000 });
|
|
38722
|
+
if (list.includes(PACKAGE_NAME))
|
|
38723
|
+
return "pnpm";
|
|
38724
|
+
} catch {}
|
|
38725
|
+
}
|
|
38726
|
+
} catch {}
|
|
38727
|
+
return "npm";
|
|
38728
|
+
}
|
|
38729
|
+
function buildInstallCommand(pm) {
|
|
38730
|
+
switch (pm) {
|
|
38731
|
+
case "bun":
|
|
38732
|
+
return `bun add -g ${PACKAGE_NAME}@latest`;
|
|
38733
|
+
case "pnpm":
|
|
38734
|
+
return `pnpm add -g ${PACKAGE_NAME}@latest`;
|
|
38735
|
+
default:
|
|
38736
|
+
return `npm install -g ${PACKAGE_NAME}@latest`;
|
|
38737
|
+
}
|
|
38738
|
+
}
|
|
38739
|
+
|
|
38740
|
+
// src/lib/update-notifier.ts
|
|
38741
|
+
import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
|
|
38742
|
+
import { existsSync as existsSync6 } from "fs";
|
|
38126
38743
|
import { homedir as homedir2 } from "os";
|
|
38127
38744
|
import path5 from "path";
|
|
38128
|
-
var
|
|
38745
|
+
var PACKAGE_NAME2 = "hermes-git";
|
|
38129
38746
|
var CHECK_INTERVAL = 24 * 60 * 60 * 1000;
|
|
38130
38747
|
var CACHE_DIR = path5.join(homedir2(), ".hermes", "cache");
|
|
38131
38748
|
var CACHE_FILE = path5.join(CACHE_DIR, "update-check.json");
|
|
38132
38749
|
async function getLatestVersion() {
|
|
38133
38750
|
try {
|
|
38134
|
-
const response = await fetch(`https://registry.npmjs.org/${
|
|
38751
|
+
const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME2}/latest`);
|
|
38135
38752
|
if (!response.ok)
|
|
38136
38753
|
return null;
|
|
38137
38754
|
const data = await response.json();
|
|
@@ -38142,9 +38759,9 @@ async function getLatestVersion() {
|
|
|
38142
38759
|
}
|
|
38143
38760
|
async function readCache() {
|
|
38144
38761
|
try {
|
|
38145
|
-
if (!
|
|
38762
|
+
if (!existsSync6(CACHE_FILE))
|
|
38146
38763
|
return null;
|
|
38147
|
-
const content = await
|
|
38764
|
+
const content = await readFile6(CACHE_FILE, "utf-8");
|
|
38148
38765
|
return JSON.parse(content);
|
|
38149
38766
|
} catch {
|
|
38150
38767
|
return null;
|
|
@@ -38152,8 +38769,8 @@ async function readCache() {
|
|
|
38152
38769
|
}
|
|
38153
38770
|
async function writeCache(cache) {
|
|
38154
38771
|
try {
|
|
38155
|
-
await
|
|
38156
|
-
await
|
|
38772
|
+
await mkdir5(CACHE_DIR, { recursive: true });
|
|
38773
|
+
await writeFile6(CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
38157
38774
|
} catch {}
|
|
38158
38775
|
}
|
|
38159
38776
|
function isNewerVersion(current, latest) {
|
|
@@ -38191,10 +38808,93 @@ async function checkForUpdates(currentVersion) {
|
|
|
38191
38808
|
} catch {}
|
|
38192
38809
|
}
|
|
38193
38810
|
|
|
38811
|
+
// src/lib/banner.ts
|
|
38812
|
+
import { readFileSync, existsSync as existsSync7 } from "fs";
|
|
38813
|
+
var ART_LINES = [
|
|
38814
|
+
"██╗ ██╗███████╗██████╗ ███╗ ███╗███████╗███████╗",
|
|
38815
|
+
"██║ ██║██╔════╝██╔══██╗████╗ ████║██╔════╝██╔════╝",
|
|
38816
|
+
"███████║█████╗ ██████╔╝██╔████╔██║█████╗ ███████╗",
|
|
38817
|
+
"██╔══██║██╔══╝ ██╔══██╗██║╚██╔╝██║██╔══╝ ╚════██║",
|
|
38818
|
+
"██║ ██║███████╗██║ ██║██║ ╚═╝ ██║███████╗███████║",
|
|
38819
|
+
"╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝"
|
|
38820
|
+
];
|
|
38821
|
+
var ROW_COLORS = [
|
|
38822
|
+
(s) => source_default.bold.cyanBright(s),
|
|
38823
|
+
(s) => source_default.bold.cyanBright(s),
|
|
38824
|
+
(s) => source_default.bold.cyan(s),
|
|
38825
|
+
(s) => source_default.bold.cyan(s),
|
|
38826
|
+
(s) => source_default.cyan(s),
|
|
38827
|
+
(s) => source_default.dim.cyan(s)
|
|
38828
|
+
];
|
|
38829
|
+
var ART_WIDTH = 52;
|
|
38830
|
+
function printBanner(version) {
|
|
38831
|
+
const wings = source_default.dim.cyan("~>))><");
|
|
38832
|
+
const tail = source_default.dim.cyan("><((<~");
|
|
38833
|
+
console.log();
|
|
38834
|
+
console.log(` ${wings}${"─".repeat(ART_WIDTH - 14)}${tail}`);
|
|
38835
|
+
for (let i = 0;i < ART_LINES.length; i++) {
|
|
38836
|
+
console.log(" " + ROW_COLORS[i](ART_LINES[i]));
|
|
38837
|
+
}
|
|
38838
|
+
const subtitle = "intent-driven git";
|
|
38839
|
+
const versionLabel = source_default.dim(`v${version}`);
|
|
38840
|
+
const dot = source_default.dim("·");
|
|
38841
|
+
const padding = " ".repeat(Math.max(0, Math.floor((ART_WIDTH - subtitle.length - version.length - 4) / 2)));
|
|
38842
|
+
console.log();
|
|
38843
|
+
console.log(` ${padding}${source_default.dim(subtitle)} ${dot} ${versionLabel}`);
|
|
38844
|
+
console.log(` ${wings}${"─".repeat(ART_WIDTH - 14)}${tail}`);
|
|
38845
|
+
console.log();
|
|
38846
|
+
}
|
|
38847
|
+
var BUILTIN_WORKFLOWS = [
|
|
38848
|
+
{ name: "pr-ready", description: "Sync, rebase, push — ready for pull request" },
|
|
38849
|
+
{ name: "daily-sync", description: "Fetch + status check to start the day" },
|
|
38850
|
+
{ name: "quick-commit", description: "Stage changes and commit with AI message" }
|
|
38851
|
+
];
|
|
38852
|
+
function loadProjectWorkflows() {
|
|
38853
|
+
try {
|
|
38854
|
+
if (!existsSync7(".hermes/config.json"))
|
|
38855
|
+
return [];
|
|
38856
|
+
const config = JSON.parse(readFileSync(".hermes/config.json", "utf-8"));
|
|
38857
|
+
const workflows = config?.workflows;
|
|
38858
|
+
if (!workflows || typeof workflows !== "object")
|
|
38859
|
+
return [];
|
|
38860
|
+
return Object.entries(workflows).filter(([name]) => !BUILTIN_WORKFLOWS.some((b) => b.name === name)).map(([name, steps]) => ({
|
|
38861
|
+
name,
|
|
38862
|
+
description: Array.isArray(steps) ? steps.join(" → ") : String(steps),
|
|
38863
|
+
custom: true
|
|
38864
|
+
}));
|
|
38865
|
+
} catch {
|
|
38866
|
+
return [];
|
|
38867
|
+
}
|
|
38868
|
+
}
|
|
38869
|
+
function printWorkflows() {
|
|
38870
|
+
const project = loadProjectWorkflows();
|
|
38871
|
+
const all = [...BUILTIN_WORKFLOWS, ...project];
|
|
38872
|
+
const nameWidth = Math.max(...all.map((w) => w.name.length)) + 2;
|
|
38873
|
+
console.log(source_default.bold(" Workflows"));
|
|
38874
|
+
console.log();
|
|
38875
|
+
for (const w of BUILTIN_WORKFLOWS) {
|
|
38876
|
+
const cmd = source_default.cyan(`hermes workflow ${w.name.padEnd(nameWidth)}`);
|
|
38877
|
+
console.log(` ${cmd} ${source_default.dim(w.description)}`);
|
|
38878
|
+
}
|
|
38879
|
+
if (project.length > 0) {
|
|
38880
|
+
console.log();
|
|
38881
|
+
console.log(source_default.dim(" Project-specific:"));
|
|
38882
|
+
for (const w of project) {
|
|
38883
|
+
const cmd = source_default.cyan(`hermes workflow ${w.name.padEnd(nameWidth)}`);
|
|
38884
|
+
console.log(` ${cmd} ${source_default.dim(w.description)}`);
|
|
38885
|
+
}
|
|
38886
|
+
}
|
|
38887
|
+
console.log();
|
|
38888
|
+
}
|
|
38889
|
+
|
|
38194
38890
|
// src/index.ts
|
|
38195
38891
|
var program2 = new Command;
|
|
38196
|
-
var CURRENT_VERSION = "0.3.
|
|
38197
|
-
program2.name("hermes").description("
|
|
38892
|
+
var CURRENT_VERSION = "0.3.2";
|
|
38893
|
+
program2.name("hermes").description("Intent-driven Git, guided by AI").version(CURRENT_VERSION).action(() => {
|
|
38894
|
+
printBanner(CURRENT_VERSION);
|
|
38895
|
+
printWorkflows();
|
|
38896
|
+
program2.help();
|
|
38897
|
+
});
|
|
38198
38898
|
initCommand(program2);
|
|
38199
38899
|
planCommand(program2);
|
|
38200
38900
|
startCommand(program2);
|
|
@@ -38205,5 +38905,7 @@ worktreeCommand(program2);
|
|
|
38205
38905
|
statsCommand(program2);
|
|
38206
38906
|
workflowCommand(program2);
|
|
38207
38907
|
configCommand(program2);
|
|
38908
|
+
guardCommand(program2);
|
|
38909
|
+
updateCommand(program2, CURRENT_VERSION);
|
|
38208
38910
|
checkForUpdates(CURRENT_VERSION).catch(() => {});
|
|
38209
38911
|
program2.parse();
|