hermes-git 0.3.0 → 0.3.1
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 +49 -0
- package/dist/index.js +712 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -171,6 +171,55 @@ Decides commit vs stash based on what's safest in your current state.
|
|
|
171
171
|
|
|
172
172
|
---
|
|
173
173
|
|
|
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
|
+
|
|
174
223
|
### `hermes conflict explain`
|
|
175
224
|
|
|
176
225
|
Understand why a conflict exists.
|
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.1";
|
|
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();
|