hermes-git 0.3.2 → 0.3.6
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 +1 -1
- package/dist/index.js +449 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -334,7 +334,7 @@ If `HERMES_PROVIDER` is not set, Hermes auto-detects by using whichever key it f
|
|
|
334
334
|
|----------|-------|-----------|
|
|
335
335
|
| Anthropic | claude-sonnet-4-6 | [console.anthropic.com](https://console.anthropic.com/) |
|
|
336
336
|
| OpenAI | gpt-4o | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) |
|
|
337
|
-
| Google | gemini-2.
|
|
337
|
+
| Google | gemini-2.5-flash | [aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey) |
|
|
338
338
|
|
|
339
339
|
---
|
|
340
340
|
|
package/dist/index.js
CHANGED
|
@@ -34494,16 +34494,26 @@ async function makeOpenAIProvider(apiKey) {
|
|
|
34494
34494
|
async function makeGeminiProvider(apiKey) {
|
|
34495
34495
|
const { GoogleGenerativeAI: GoogleGenerativeAI2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
34496
34496
|
const client = new GoogleGenerativeAI2(apiKey);
|
|
34497
|
-
const genModel = client.getGenerativeModel({ model: "gemini-2.
|
|
34497
|
+
const genModel = client.getGenerativeModel({ model: "gemini-2.5-flash" });
|
|
34498
34498
|
return {
|
|
34499
34499
|
name: "Gemini",
|
|
34500
|
-
model: "gemini-2.
|
|
34500
|
+
model: "gemini-2.5-flash",
|
|
34501
34501
|
async complete(prompt) {
|
|
34502
|
-
|
|
34503
|
-
|
|
34504
|
-
|
|
34505
|
-
|
|
34506
|
-
|
|
34502
|
+
try {
|
|
34503
|
+
const result = await genModel.generateContent(prompt);
|
|
34504
|
+
const text = result.response.text();
|
|
34505
|
+
if (!text)
|
|
34506
|
+
throw new Error("Empty response from Gemini");
|
|
34507
|
+
return text.trim();
|
|
34508
|
+
} catch (err) {
|
|
34509
|
+
const msg = err?.message ?? "";
|
|
34510
|
+
if (msg.includes("429") || msg.includes("Too Many Requests") || msg.includes("quota")) {
|
|
34511
|
+
const retryMatch = msg.match(/retry in ([\d.]+)s/i);
|
|
34512
|
+
const retryIn = retryMatch ? `${Math.ceil(parseFloat(retryMatch[1]))}s` : null;
|
|
34513
|
+
throw new GeminiQuotaError(retryIn);
|
|
34514
|
+
}
|
|
34515
|
+
throw err;
|
|
34516
|
+
}
|
|
34507
34517
|
}
|
|
34508
34518
|
};
|
|
34509
34519
|
}
|
|
@@ -34548,6 +34558,25 @@ async function getProvider() {
|
|
|
34548
34558
|
throw new NoProviderError;
|
|
34549
34559
|
}
|
|
34550
34560
|
|
|
34561
|
+
class GeminiQuotaError extends Error {
|
|
34562
|
+
constructor(retryIn) {
|
|
34563
|
+
const lines = [
|
|
34564
|
+
"",
|
|
34565
|
+
source_default.red("❌ Gemini quota exceeded"),
|
|
34566
|
+
"",
|
|
34567
|
+
retryIn ? source_default.white(` Per-minute limit hit. You could retry in ${source_default.bold(retryIn)}.`) : source_default.white(" Your free-tier daily quota is exhausted."),
|
|
34568
|
+
"",
|
|
34569
|
+
source_default.bold(" Options:"),
|
|
34570
|
+
` ${source_default.cyan("1.")} Enable billing at ${source_default.dim("https://aistudio.google.com/app/billing")}`,
|
|
34571
|
+
` ${source_default.cyan("2.")} Switch provider: ${source_default.dim("hermes config set provider anthropic")}`,
|
|
34572
|
+
` ${source_default.cyan("3.")} Switch provider: ${source_default.dim("hermes config set provider openai")}`,
|
|
34573
|
+
""
|
|
34574
|
+
];
|
|
34575
|
+
super(lines.join(`
|
|
34576
|
+
`));
|
|
34577
|
+
}
|
|
34578
|
+
}
|
|
34579
|
+
|
|
34551
34580
|
class MissingKeyError extends Error {
|
|
34552
34581
|
constructor(provider) {
|
|
34553
34582
|
const configs = {
|
|
@@ -37589,9 +37618,11 @@ function initCommand(program2) {
|
|
|
37589
37618
|
await mkdir3(".hermes", { recursive: true });
|
|
37590
37619
|
await writeFile4(".hermes/config.json", JSON.stringify(config, null, 2));
|
|
37591
37620
|
await mkdir3(".hermes/backups", { recursive: true });
|
|
37592
|
-
await
|
|
37593
|
-
.
|
|
37594
|
-
`
|
|
37621
|
+
const added = await updateGitignore();
|
|
37622
|
+
if (added.length > 0) {
|
|
37623
|
+
console.log(`
|
|
37624
|
+
\uD83D\uDCDD Added to .gitignore: ${added.join(", ")}`);
|
|
37625
|
+
}
|
|
37595
37626
|
displaySuccess("Hermes initialized successfully!");
|
|
37596
37627
|
console.log(`
|
|
37597
37628
|
\uD83D\uDCC4 Configuration saved to .hermes/config.json`);
|
|
@@ -37734,23 +37765,38 @@ async function interactiveConfig(repoInfo) {
|
|
|
37734
37765
|
}
|
|
37735
37766
|
};
|
|
37736
37767
|
}
|
|
37737
|
-
|
|
37738
|
-
|
|
37739
|
-
|
|
37740
|
-
|
|
37741
|
-
|
|
37742
|
-
|
|
37743
|
-
|
|
37744
|
-
|
|
37745
|
-
|
|
37768
|
+
var GITIGNORE_ENTRIES = [
|
|
37769
|
+
{ pattern: ".env", comment: null },
|
|
37770
|
+
{ pattern: ".env.*", comment: null },
|
|
37771
|
+
{ pattern: "!.env.example", comment: null },
|
|
37772
|
+
{ pattern: ".hermes/backups/", comment: null },
|
|
37773
|
+
{ pattern: ".hermes/stats.json", comment: null }
|
|
37774
|
+
];
|
|
37775
|
+
async function updateGitignore() {
|
|
37776
|
+
const { appendFile, readFile: rf } = await import("fs/promises");
|
|
37777
|
+
const gitignorePath = ".gitignore";
|
|
37778
|
+
let existing = "";
|
|
37779
|
+
if (existsSync4(gitignorePath)) {
|
|
37780
|
+
try {
|
|
37781
|
+
existing = await rf(gitignorePath, "utf-8");
|
|
37782
|
+
} catch {}
|
|
37783
|
+
}
|
|
37784
|
+
const existingLines = existing.split(`
|
|
37785
|
+
`).map((l) => l.trim());
|
|
37786
|
+
const toAdd = GITIGNORE_ENTRIES.filter((e) => !existingLines.includes(e.pattern));
|
|
37787
|
+
if (toAdd.length === 0)
|
|
37788
|
+
return [];
|
|
37789
|
+
const block = `
|
|
37746
37790
|
# Hermes
|
|
37747
|
-
` +
|
|
37748
|
-
|
|
37749
|
-
|
|
37750
|
-
|
|
37751
|
-
|
|
37752
|
-
|
|
37753
|
-
|
|
37791
|
+
` + toAdd.map((e) => e.pattern).join(`
|
|
37792
|
+
`) + `
|
|
37793
|
+
`;
|
|
37794
|
+
if (existsSync4(gitignorePath)) {
|
|
37795
|
+
await appendFile(gitignorePath, block);
|
|
37796
|
+
} else {
|
|
37797
|
+
await writeFile4(gitignorePath, block.trimStart());
|
|
37798
|
+
}
|
|
37799
|
+
return toAdd.map((e) => e.pattern);
|
|
37754
37800
|
}
|
|
37755
37801
|
|
|
37756
37802
|
// src/commands/stats.ts
|
|
@@ -38737,32 +38783,348 @@ function buildInstallCommand(pm) {
|
|
|
38737
38783
|
}
|
|
38738
38784
|
}
|
|
38739
38785
|
|
|
38786
|
+
// src/commands/commit.ts
|
|
38787
|
+
import { exec as exec6 } from "child_process";
|
|
38788
|
+
import { promisify as promisify6 } from "util";
|
|
38789
|
+
var execAsync6 = promisify6(exec6);
|
|
38790
|
+
async function getStagedFiles2() {
|
|
38791
|
+
try {
|
|
38792
|
+
const { stdout } = await execAsync6("git diff --cached --name-status");
|
|
38793
|
+
return stdout.trim().split(`
|
|
38794
|
+
`).filter(Boolean).map((line) => {
|
|
38795
|
+
const [status, ...rest] = line.split("\t");
|
|
38796
|
+
return { status: status.trim(), path: rest.join("\t").trim() };
|
|
38797
|
+
});
|
|
38798
|
+
} catch {
|
|
38799
|
+
return [];
|
|
38800
|
+
}
|
|
38801
|
+
}
|
|
38802
|
+
async function getUnstagedFiles() {
|
|
38803
|
+
try {
|
|
38804
|
+
const { stdout } = await execAsync6("git status --porcelain");
|
|
38805
|
+
return stdout.trim().split(`
|
|
38806
|
+
`).filter(Boolean).map((line) => {
|
|
38807
|
+
const status = line.slice(0, 2).trim();
|
|
38808
|
+
const path5 = line.slice(3).trim();
|
|
38809
|
+
return { status, path: path5 };
|
|
38810
|
+
}).filter(({ status }) => status === "??" || status[0] === " " || status.length === 1);
|
|
38811
|
+
} catch {
|
|
38812
|
+
return [];
|
|
38813
|
+
}
|
|
38814
|
+
}
|
|
38815
|
+
async function getRecentBranchCommits(n = 5) {
|
|
38816
|
+
try {
|
|
38817
|
+
const { stdout } = await execAsync6(`git log --oneline -${n} --no-merges 2>/dev/null`);
|
|
38818
|
+
return stdout.trim();
|
|
38819
|
+
} catch {
|
|
38820
|
+
return "";
|
|
38821
|
+
}
|
|
38822
|
+
}
|
|
38823
|
+
function statusLabel(s) {
|
|
38824
|
+
const map = {
|
|
38825
|
+
M: source_default.yellow("modified "),
|
|
38826
|
+
A: source_default.green("new file "),
|
|
38827
|
+
D: source_default.red("deleted "),
|
|
38828
|
+
R: source_default.blue("renamed "),
|
|
38829
|
+
C: source_default.blue("copied "),
|
|
38830
|
+
"??": source_default.dim("untracked"),
|
|
38831
|
+
" M": source_default.yellow("modified "),
|
|
38832
|
+
" D": source_default.red("deleted ")
|
|
38833
|
+
};
|
|
38834
|
+
return map[s] ?? source_default.dim(s.padEnd(9));
|
|
38835
|
+
}
|
|
38836
|
+
function divider() {
|
|
38837
|
+
console.log(source_default.dim(" " + "─".repeat(54)));
|
|
38838
|
+
}
|
|
38839
|
+
async function analyzeChanges(branch, stagedDiff, stagedStat, recentCommits) {
|
|
38840
|
+
const prompt2 = `You are an expert Git historian. Analyze these staged changes and produce a structured commit analysis.
|
|
38841
|
+
|
|
38842
|
+
Branch: ${branch}
|
|
38843
|
+
Recent commits on this branch:
|
|
38844
|
+
${recentCommits || "(none — first commit)"}
|
|
38845
|
+
|
|
38846
|
+
Staged diff summary:
|
|
38847
|
+
${stagedStat}
|
|
38848
|
+
|
|
38849
|
+
Full staged diff (truncated to 12000 chars):
|
|
38850
|
+
${stagedDiff.slice(0, 12000)}
|
|
38851
|
+
|
|
38852
|
+
Return RAW JSON ONLY — no markdown, no code fences, just the JSON object:
|
|
38853
|
+
{
|
|
38854
|
+
"summary": "2-3 sentences: what was changed and the likely intent behind it",
|
|
38855
|
+
"concerns": ["array of concerns — mixed unrelated changes, debug/temp code, large binary, TODO left in, test missing, etc. Empty array if none."],
|
|
38856
|
+
"message": "conventional commit: type(scope): subject (max 72 chars). Be specific.",
|
|
38857
|
+
"body": "optional multi-line body explaining WHY (not WHAT). Empty string if the subject is self-explanatory.",
|
|
38858
|
+
"alternatives": ["two alternative commit messages if the intent is ambiguous"]
|
|
38859
|
+
}
|
|
38860
|
+
|
|
38861
|
+
Rules for message:
|
|
38862
|
+
- type: feat | fix | refactor | docs | test | chore | style | perf | build | ci
|
|
38863
|
+
- scope: the primary module/component affected (omit if changes are cross-cutting)
|
|
38864
|
+
- subject: imperative mood, no period, lowercase after colon
|
|
38865
|
+
- be specific — "add OAuth login" not "add feature"`;
|
|
38866
|
+
const raw = await getAISuggestion(prompt2);
|
|
38867
|
+
const cleaned = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
|
|
38868
|
+
try {
|
|
38869
|
+
return JSON.parse(cleaned);
|
|
38870
|
+
} catch {
|
|
38871
|
+
return {
|
|
38872
|
+
summary: "Could not parse analysis.",
|
|
38873
|
+
concerns: [],
|
|
38874
|
+
message: cleaned.split(`
|
|
38875
|
+
`)[0].slice(0, 72),
|
|
38876
|
+
body: "",
|
|
38877
|
+
alternatives: []
|
|
38878
|
+
};
|
|
38879
|
+
}
|
|
38880
|
+
}
|
|
38881
|
+
function commitCommand(program2) {
|
|
38882
|
+
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) => {
|
|
38883
|
+
const start = Date.now();
|
|
38884
|
+
try {
|
|
38885
|
+
const repoState = await getRepoState();
|
|
38886
|
+
if (options.all) {
|
|
38887
|
+
displayStep("git add -u");
|
|
38888
|
+
await execAsync6("git add -u");
|
|
38889
|
+
}
|
|
38890
|
+
const staged = await getStagedFiles2();
|
|
38891
|
+
if (staged.length === 0) {
|
|
38892
|
+
console.log(source_default.yellow(`
|
|
38893
|
+
Nothing staged to commit.`));
|
|
38894
|
+
console.log(source_default.dim(" Use `git add <file>` or run `hermes commit --all` to stage tracked changes.\n"));
|
|
38895
|
+
process.exit(0);
|
|
38896
|
+
}
|
|
38897
|
+
const unstaged = await getUnstagedFiles();
|
|
38898
|
+
const recentCommits = await getRecentBranchCommits(5);
|
|
38899
|
+
console.log();
|
|
38900
|
+
console.log(source_default.bold(` Staged (${staged.length} file${staged.length === 1 ? "" : "s"}):`));
|
|
38901
|
+
for (const f of staged) {
|
|
38902
|
+
console.log(` ${statusLabel(f.status)} ${source_default.white(f.path)}`);
|
|
38903
|
+
}
|
|
38904
|
+
const relevantUnstaged = unstaged.filter((u) => !staged.some((s) => s.path === u.path));
|
|
38905
|
+
if (relevantUnstaged.length > 0) {
|
|
38906
|
+
console.log();
|
|
38907
|
+
console.log(source_default.dim(` Not staged (${relevantUnstaged.length}):`));
|
|
38908
|
+
for (const f of relevantUnstaged.slice(0, 8)) {
|
|
38909
|
+
console.log(` ${statusLabel(f.status)} ${source_default.dim(f.path)}`);
|
|
38910
|
+
}
|
|
38911
|
+
if (relevantUnstaged.length > 8) {
|
|
38912
|
+
console.log(source_default.dim(` ...and ${relevantUnstaged.length - 8} more`));
|
|
38913
|
+
}
|
|
38914
|
+
}
|
|
38915
|
+
const { name, model } = await getActiveProvider();
|
|
38916
|
+
console.log();
|
|
38917
|
+
console.log(` Analyzing... ${source_default.dim(`[${name} / ${model}]`)}`);
|
|
38918
|
+
const [stagedDiff, stagedStat] = await Promise.all([
|
|
38919
|
+
execAsync6("git diff --cached").then((r) => r.stdout),
|
|
38920
|
+
execAsync6("git diff --cached --stat").then((r) => r.stdout)
|
|
38921
|
+
]);
|
|
38922
|
+
const analysis = await analyzeChanges(repoState.currentBranch, stagedDiff, stagedStat, recentCommits);
|
|
38923
|
+
console.log();
|
|
38924
|
+
divider();
|
|
38925
|
+
console.log();
|
|
38926
|
+
console.log(" " + source_default.bold("Understanding"));
|
|
38927
|
+
console.log();
|
|
38928
|
+
const words = analysis.summary.split(" ");
|
|
38929
|
+
let line = " ";
|
|
38930
|
+
for (const w of words) {
|
|
38931
|
+
if (line.length + w.length > 62) {
|
|
38932
|
+
console.log(source_default.white(line));
|
|
38933
|
+
line = " " + w + " ";
|
|
38934
|
+
} else {
|
|
38935
|
+
line += w + " ";
|
|
38936
|
+
}
|
|
38937
|
+
}
|
|
38938
|
+
if (line.trim())
|
|
38939
|
+
console.log(source_default.white(line));
|
|
38940
|
+
if (analysis.concerns.length > 0) {
|
|
38941
|
+
console.log();
|
|
38942
|
+
for (const c of analysis.concerns) {
|
|
38943
|
+
console.log(` ${source_default.yellow("⚠")} ${source_default.yellow(c)}`);
|
|
38944
|
+
}
|
|
38945
|
+
}
|
|
38946
|
+
console.log();
|
|
38947
|
+
divider();
|
|
38948
|
+
console.log();
|
|
38949
|
+
const hasBody = options.body !== false && analysis.body.trim().length > 0;
|
|
38950
|
+
console.log(" " + source_default.bold("Proposed commit"));
|
|
38951
|
+
console.log();
|
|
38952
|
+
console.log(" " + source_default.cyan.bold(analysis.message));
|
|
38953
|
+
if (hasBody) {
|
|
38954
|
+
console.log();
|
|
38955
|
+
analysis.body.split(`
|
|
38956
|
+
`).forEach((l) => {
|
|
38957
|
+
console.log(" " + source_default.dim(l));
|
|
38958
|
+
});
|
|
38959
|
+
}
|
|
38960
|
+
console.log();
|
|
38961
|
+
let finalMessage = analysis.message;
|
|
38962
|
+
let finalBody = hasBody ? analysis.body : "";
|
|
38963
|
+
let committed = false;
|
|
38964
|
+
while (!committed) {
|
|
38965
|
+
const choices = [
|
|
38966
|
+
{ name: "Commit", value: "commit" },
|
|
38967
|
+
{ name: "Edit message", value: "edit" }
|
|
38968
|
+
];
|
|
38969
|
+
if (analysis.alternatives?.length > 0) {
|
|
38970
|
+
choices.push({ name: "See alternative messages", value: "alts" });
|
|
38971
|
+
}
|
|
38972
|
+
if (relevantUnstaged.length > 0) {
|
|
38973
|
+
choices.push({ name: "Stage more files first", value: "stage" });
|
|
38974
|
+
}
|
|
38975
|
+
choices.push({ name: "Cancel", value: "cancel" });
|
|
38976
|
+
const { action } = await esm_default12.prompt([
|
|
38977
|
+
{
|
|
38978
|
+
type: "list",
|
|
38979
|
+
name: "action",
|
|
38980
|
+
message: "What would you like to do?",
|
|
38981
|
+
choices
|
|
38982
|
+
}
|
|
38983
|
+
]);
|
|
38984
|
+
if (action === "cancel") {
|
|
38985
|
+
console.log(source_default.dim(`
|
|
38986
|
+
Cancelled. Changes remain staged.
|
|
38987
|
+
`));
|
|
38988
|
+
await recordCommand("commit", [], Date.now() - start, false);
|
|
38989
|
+
return;
|
|
38990
|
+
}
|
|
38991
|
+
if (action === "alts") {
|
|
38992
|
+
console.log();
|
|
38993
|
+
analysis.alternatives.forEach((alt, i) => {
|
|
38994
|
+
console.log(` ${source_default.dim(`${i + 1}.`)} ${source_default.cyan(alt)}`);
|
|
38995
|
+
});
|
|
38996
|
+
console.log();
|
|
38997
|
+
const { pick } = await esm_default12.prompt([
|
|
38998
|
+
{
|
|
38999
|
+
type: "list",
|
|
39000
|
+
name: "pick",
|
|
39001
|
+
message: "Use an alternative?",
|
|
39002
|
+
choices: [
|
|
39003
|
+
...analysis.alternatives.map((a, i) => ({ name: a, value: `${i}` })),
|
|
39004
|
+
{ name: "Keep original", value: "keep" }
|
|
39005
|
+
]
|
|
39006
|
+
}
|
|
39007
|
+
]);
|
|
39008
|
+
if (pick !== "keep") {
|
|
39009
|
+
finalMessage = analysis.alternatives[parseInt(pick, 10)];
|
|
39010
|
+
finalBody = "";
|
|
39011
|
+
console.log();
|
|
39012
|
+
console.log(" " + source_default.cyan.bold(finalMessage));
|
|
39013
|
+
console.log();
|
|
39014
|
+
}
|
|
39015
|
+
continue;
|
|
39016
|
+
}
|
|
39017
|
+
if (action === "edit") {
|
|
39018
|
+
const { edited } = await esm_default12.prompt([
|
|
39019
|
+
{
|
|
39020
|
+
type: "input",
|
|
39021
|
+
name: "edited",
|
|
39022
|
+
message: "Commit message:",
|
|
39023
|
+
default: finalMessage
|
|
39024
|
+
}
|
|
39025
|
+
]);
|
|
39026
|
+
finalMessage = edited.trim();
|
|
39027
|
+
finalBody = "";
|
|
39028
|
+
console.log();
|
|
39029
|
+
continue;
|
|
39030
|
+
}
|
|
39031
|
+
if (action === "stage") {
|
|
39032
|
+
const { files } = await esm_default12.prompt([
|
|
39033
|
+
{
|
|
39034
|
+
type: "checkbox",
|
|
39035
|
+
name: "files",
|
|
39036
|
+
message: "Select files to stage:",
|
|
39037
|
+
choices: relevantUnstaged.map((f) => ({
|
|
39038
|
+
name: `${statusLabel(f.status)} ${f.path}`,
|
|
39039
|
+
value: f.path
|
|
39040
|
+
}))
|
|
39041
|
+
}
|
|
39042
|
+
]);
|
|
39043
|
+
if (files.length > 0) {
|
|
39044
|
+
for (const f of files) {
|
|
39045
|
+
const cmd = `git add ${JSON.stringify(f)}`;
|
|
39046
|
+
displayStep(cmd);
|
|
39047
|
+
await execAsync6(cmd);
|
|
39048
|
+
}
|
|
39049
|
+
console.log(source_default.dim(`
|
|
39050
|
+
Staged ${files.length} file(s). Re-running analysis...
|
|
39051
|
+
`));
|
|
39052
|
+
const [newDiff, newStat] = await Promise.all([
|
|
39053
|
+
execAsync6("git diff --cached").then((r) => r.stdout),
|
|
39054
|
+
execAsync6("git diff --cached --stat").then((r) => r.stdout)
|
|
39055
|
+
]);
|
|
39056
|
+
const newAnalysis = await analyzeChanges(repoState.currentBranch, newDiff, newStat, recentCommits);
|
|
39057
|
+
analysis.summary = newAnalysis.summary;
|
|
39058
|
+
analysis.concerns = newAnalysis.concerns;
|
|
39059
|
+
analysis.message = newAnalysis.message;
|
|
39060
|
+
analysis.body = newAnalysis.body;
|
|
39061
|
+
analysis.alternatives = newAnalysis.alternatives;
|
|
39062
|
+
finalMessage = newAnalysis.message;
|
|
39063
|
+
finalBody = options.body !== false && newAnalysis.body.trim().length > 0 ? newAnalysis.body : "";
|
|
39064
|
+
console.log(" " + source_default.bold("Updated proposal"));
|
|
39065
|
+
console.log();
|
|
39066
|
+
console.log(" " + source_default.cyan.bold(finalMessage));
|
|
39067
|
+
if (finalBody) {
|
|
39068
|
+
console.log();
|
|
39069
|
+
finalBody.split(`
|
|
39070
|
+
`).forEach((l) => console.log(" " + source_default.dim(l)));
|
|
39071
|
+
}
|
|
39072
|
+
if (newAnalysis.concerns.length > 0) {
|
|
39073
|
+
console.log();
|
|
39074
|
+
newAnalysis.concerns.forEach((c) => {
|
|
39075
|
+
console.log(` ${source_default.yellow("⚠")} ${source_default.yellow(c)}`);
|
|
39076
|
+
});
|
|
39077
|
+
}
|
|
39078
|
+
console.log();
|
|
39079
|
+
}
|
|
39080
|
+
continue;
|
|
39081
|
+
}
|
|
39082
|
+
if (action === "commit") {
|
|
39083
|
+
const fullMessage = finalBody ? `${finalMessage}
|
|
39084
|
+
|
|
39085
|
+
${finalBody}` : finalMessage;
|
|
39086
|
+
displayStep(`git commit -m "${finalMessage}"${finalBody ? " (with body)" : ""}`);
|
|
39087
|
+
await execAsync6(`git commit -m ${JSON.stringify(fullMessage)}`);
|
|
39088
|
+
displaySuccess("Committed!");
|
|
39089
|
+
const { stdout: hash } = await execAsync6("git rev-parse --short HEAD");
|
|
39090
|
+
console.log(source_default.dim(` ${hash.trim()} ${finalMessage}
|
|
39091
|
+
`));
|
|
39092
|
+
await recordCommand("commit", [], Date.now() - start, true);
|
|
39093
|
+
committed = true;
|
|
39094
|
+
}
|
|
39095
|
+
}
|
|
39096
|
+
} catch (error3) {
|
|
39097
|
+
console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
|
|
39098
|
+
await recordCommand("commit", [], Date.now() - start, false);
|
|
39099
|
+
process.exit(1);
|
|
39100
|
+
}
|
|
39101
|
+
});
|
|
39102
|
+
}
|
|
39103
|
+
|
|
38740
39104
|
// src/lib/update-notifier.ts
|
|
38741
39105
|
import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
|
|
38742
39106
|
import { existsSync as existsSync6 } from "fs";
|
|
38743
39107
|
import { homedir as homedir2 } from "os";
|
|
38744
39108
|
import path5 from "path";
|
|
38745
39109
|
var PACKAGE_NAME2 = "hermes-git";
|
|
38746
|
-
var CHECK_INTERVAL =
|
|
39110
|
+
var CHECK_INTERVAL = 12 * 60 * 60 * 1000;
|
|
38747
39111
|
var CACHE_DIR = path5.join(homedir2(), ".hermes", "cache");
|
|
38748
39112
|
var CACHE_FILE = path5.join(CACHE_DIR, "update-check.json");
|
|
38749
|
-
async function
|
|
39113
|
+
async function fetchDistTags() {
|
|
38750
39114
|
try {
|
|
38751
|
-
const
|
|
38752
|
-
if (!
|
|
38753
|
-
return
|
|
38754
|
-
|
|
38755
|
-
return data.version || null;
|
|
39115
|
+
const res = await fetch(`https://registry.npmjs.org/-/package/${PACKAGE_NAME2}/dist-tags`, { signal: AbortSignal.timeout(5000) });
|
|
39116
|
+
if (!res.ok)
|
|
39117
|
+
return {};
|
|
39118
|
+
return await res.json();
|
|
38756
39119
|
} catch {
|
|
38757
|
-
return
|
|
39120
|
+
return {};
|
|
38758
39121
|
}
|
|
38759
39122
|
}
|
|
38760
39123
|
async function readCache() {
|
|
38761
39124
|
try {
|
|
38762
39125
|
if (!existsSync6(CACHE_FILE))
|
|
38763
39126
|
return null;
|
|
38764
|
-
|
|
38765
|
-
return JSON.parse(content);
|
|
39127
|
+
return JSON.parse(await readFile6(CACHE_FILE, "utf-8"));
|
|
38766
39128
|
} catch {
|
|
38767
39129
|
return null;
|
|
38768
39130
|
}
|
|
@@ -38773,36 +39135,66 @@ async function writeCache(cache) {
|
|
|
38773
39135
|
await writeFile6(CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
38774
39136
|
} catch {}
|
|
38775
39137
|
}
|
|
38776
|
-
function
|
|
38777
|
-
const
|
|
38778
|
-
const
|
|
39138
|
+
function compareVersions2(a, b) {
|
|
39139
|
+
const pa = a.split(".").map(Number);
|
|
39140
|
+
const pb = b.split(".").map(Number);
|
|
38779
39141
|
for (let i = 0;i < 3; i++) {
|
|
38780
|
-
if (
|
|
38781
|
-
return
|
|
38782
|
-
if (
|
|
38783
|
-
return
|
|
39142
|
+
if (pa[i] > pb[i])
|
|
39143
|
+
return 1;
|
|
39144
|
+
if (pa[i] < pb[i])
|
|
39145
|
+
return -1;
|
|
38784
39146
|
}
|
|
38785
|
-
return
|
|
39147
|
+
return 0;
|
|
39148
|
+
}
|
|
39149
|
+
async function enforceMinimumVersion(currentVersion) {
|
|
39150
|
+
try {
|
|
39151
|
+
const cache = await readCache();
|
|
39152
|
+
const now = Date.now();
|
|
39153
|
+
const stale = !cache || now - cache.lastChecked > CHECK_INTERVAL;
|
|
39154
|
+
let minimumVersion = cache?.minimumVersion ?? null;
|
|
39155
|
+
let latestVersion = cache?.latestVersion ?? null;
|
|
39156
|
+
if (stale) {
|
|
39157
|
+
const tags = await fetchDistTags();
|
|
39158
|
+
latestVersion = tags.latest ?? latestVersion;
|
|
39159
|
+
minimumVersion = tags.minimum ?? null;
|
|
39160
|
+
await writeCache({ lastChecked: now, latestVersion: latestVersion ?? undefined, minimumVersion: minimumVersion ?? undefined });
|
|
39161
|
+
}
|
|
39162
|
+
if (minimumVersion && compareVersions2(currentVersion, minimumVersion) < 0) {
|
|
39163
|
+
console.error("");
|
|
39164
|
+
console.error(source_default.red.bold(" ✖ This version of hermes is no longer supported."));
|
|
39165
|
+
console.error("");
|
|
39166
|
+
console.error(` You have ${source_default.dim(currentVersion)}, minimum required is ${source_default.red.bold(minimumVersion)}.`);
|
|
39167
|
+
console.error("");
|
|
39168
|
+
console.error(" Update now:");
|
|
39169
|
+
console.error(` ${source_default.cyan("npm install -g hermes-git@latest")}`);
|
|
39170
|
+
console.error(` ${source_default.dim("or: hermes update")}`);
|
|
39171
|
+
console.error("");
|
|
39172
|
+
process.exit(1);
|
|
39173
|
+
}
|
|
39174
|
+
} catch {}
|
|
38786
39175
|
}
|
|
38787
39176
|
async function checkForUpdates(currentVersion) {
|
|
38788
39177
|
try {
|
|
38789
39178
|
const cache = await readCache();
|
|
38790
39179
|
const now = Date.now();
|
|
38791
|
-
const
|
|
38792
|
-
let latestVersion = cache?.latestVersion
|
|
38793
|
-
if (
|
|
38794
|
-
|
|
39180
|
+
const stale = !cache || now - cache.lastChecked > CHECK_INTERVAL;
|
|
39181
|
+
let latestVersion = cache?.latestVersion ?? null;
|
|
39182
|
+
if (stale) {
|
|
39183
|
+
const tags = await fetchDistTags();
|
|
39184
|
+
latestVersion = tags.latest ?? null;
|
|
38795
39185
|
await writeCache({
|
|
38796
39186
|
lastChecked: now,
|
|
38797
|
-
latestVersion: latestVersion
|
|
39187
|
+
latestVersion: latestVersion ?? undefined,
|
|
39188
|
+
minimumVersion: cache?.minimumVersion
|
|
38798
39189
|
});
|
|
38799
39190
|
}
|
|
38800
|
-
if (latestVersion &&
|
|
39191
|
+
if (latestVersion && compareVersions2(latestVersion, currentVersion) > 0) {
|
|
39192
|
+
const gap = " ".repeat(Math.max(0, 17 - currentVersion.length - latestVersion.length));
|
|
38801
39193
|
console.log(source_default.yellow(`
|
|
38802
|
-
|
|
38803
|
-
console.log(source_default.yellow("│") +
|
|
38804
|
-
console.log(source_default.yellow("│") +
|
|
38805
|
-
console.log(source_default.yellow(
|
|
39194
|
+
┌──────────────────────────────────────────────────────┐`));
|
|
39195
|
+
console.log(source_default.yellow("│") + ` ${source_default.bold("Update available!")} ${source_default.dim(currentVersion)} → ${source_default.green.bold(latestVersion)}${gap}` + source_default.yellow("│"));
|
|
39196
|
+
console.log(source_default.yellow("│") + ` Run: ${source_default.cyan("hermes update")}` + " ".repeat(36) + source_default.yellow("│"));
|
|
39197
|
+
console.log(source_default.yellow(`└──────────────────────────────────────────────────────┘
|
|
38806
39198
|
`));
|
|
38807
39199
|
}
|
|
38808
39200
|
} catch {}
|
|
@@ -38889,7 +39281,7 @@ function printWorkflows() {
|
|
|
38889
39281
|
|
|
38890
39282
|
// src/index.ts
|
|
38891
39283
|
var program2 = new Command;
|
|
38892
|
-
var CURRENT_VERSION = "0.3.
|
|
39284
|
+
var CURRENT_VERSION = "0.3.6";
|
|
38893
39285
|
program2.name("hermes").description("Intent-driven Git, guided by AI").version(CURRENT_VERSION).action(() => {
|
|
38894
39286
|
printBanner(CURRENT_VERSION);
|
|
38895
39287
|
printWorkflows();
|
|
@@ -38907,5 +39299,5 @@ workflowCommand(program2);
|
|
|
38907
39299
|
configCommand(program2);
|
|
38908
39300
|
guardCommand(program2);
|
|
38909
39301
|
updateCommand(program2, CURRENT_VERSION);
|
|
38910
|
-
|
|
38911
|
-
program2.
|
|
39302
|
+
commitCommand(program2);
|
|
39303
|
+
enforceMinimumVersion(CURRENT_VERSION).then(() => program2.parseAsync()).then(() => checkForUpdates(CURRENT_VERSION)).catch(() => {});
|