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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +449 -57
  3. 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.0-flash | [aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey) |
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.0-flash" });
34497
+ const genModel = client.getGenerativeModel({ model: "gemini-2.5-flash" });
34498
34498
  return {
34499
34499
  name: "Gemini",
34500
- model: "gemini-2.0-flash",
34500
+ model: "gemini-2.5-flash",
34501
34501
  async complete(prompt) {
34502
- const result = await genModel.generateContent(prompt);
34503
- const text = result.response.text();
34504
- if (!text)
34505
- throw new Error("Empty response from Gemini");
34506
- return text.trim();
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 appendToGitignore(`.hermes/backups/
37593
- .hermes/stats.json
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
- async function appendToGitignore(content) {
37738
- try {
37739
- const { appendFile } = await import("fs/promises");
37740
- const gitignorePath = ".gitignore";
37741
- if (existsSync4(gitignorePath)) {
37742
- const { readFile: readFile5 } = await import("fs/promises");
37743
- const existing = await readFile5(gitignorePath, "utf-8");
37744
- if (!existing.includes(".hermes/backups")) {
37745
- await appendFile(gitignorePath, `
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
- ` + content);
37748
- }
37749
- } else {
37750
- await writeFile4(gitignorePath, `# Hermes
37751
- ` + content);
37752
- }
37753
- } catch {}
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 = 24 * 60 * 60 * 1000;
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 getLatestVersion() {
39113
+ async function fetchDistTags() {
38750
39114
  try {
38751
- const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME2}/latest`);
38752
- if (!response.ok)
38753
- return null;
38754
- const data = await response.json();
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 null;
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
- const content = await readFile6(CACHE_FILE, "utf-8");
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 isNewerVersion(current, latest) {
38777
- const currentParts = current.split(".").map(Number);
38778
- const latestParts = latest.split(".").map(Number);
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 (latestParts[i] > currentParts[i])
38781
- return true;
38782
- if (latestParts[i] < currentParts[i])
38783
- return false;
39142
+ if (pa[i] > pb[i])
39143
+ return 1;
39144
+ if (pa[i] < pb[i])
39145
+ return -1;
38784
39146
  }
38785
- return false;
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 shouldCheck = !cache || now - cache.lastChecked > CHECK_INTERVAL;
38792
- let latestVersion = cache?.latestVersion || null;
38793
- if (shouldCheck) {
38794
- latestVersion = await getLatestVersion();
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 || undefined
39187
+ latestVersion: latestVersion ?? undefined,
39188
+ minimumVersion: cache?.minimumVersion
38798
39189
  });
38799
39190
  }
38800
- if (latestVersion && isNewerVersion(currentVersion, 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("│") + " " + source_default.bold("Update available!") + " " + source_default.dim(currentVersion) + " " + source_default.green.bold(latestVersion) + " " + source_default.yellow("│"));
38804
- console.log(source_default.yellow("│") + " Run: " + source_default.cyan("npm install -g hermes-git@latest") + " " + 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.2";
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
- checkForUpdates(CURRENT_VERSION).catch(() => {});
38911
- program2.parse();
39302
+ commitCommand(program2);
39303
+ enforceMinimumVersion(CURRENT_VERSION).then(() => program2.parseAsync()).then(() => checkForUpdates(CURRENT_VERSION)).catch(() => {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermes-git",
3
- "version": "0.3.2",
3
+ "version": "0.3.6",
4
4
  "description": "Intent-driven Git, guided by AI. Turn natural language into safe, explainable Git operations.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",