hermes-git 0.3.7 → 0.3.8
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/dist/index.js +1541 -672
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14820,15 +14820,15 @@ var require_route = __commonJS((exports, module) => {
|
|
|
14820
14820
|
};
|
|
14821
14821
|
}
|
|
14822
14822
|
function wrapConversion(toModel, graph) {
|
|
14823
|
-
const
|
|
14823
|
+
const path5 = [graph[toModel].parent, toModel];
|
|
14824
14824
|
let fn = conversions[graph[toModel].parent][toModel];
|
|
14825
14825
|
let cur = graph[toModel].parent;
|
|
14826
14826
|
while (graph[cur].parent) {
|
|
14827
|
-
|
|
14827
|
+
path5.unshift(graph[cur].parent);
|
|
14828
14828
|
fn = link(conversions[graph[cur].parent][cur], fn);
|
|
14829
14829
|
cur = graph[cur].parent;
|
|
14830
14830
|
}
|
|
14831
|
-
fn.conversion =
|
|
14831
|
+
fn.conversion = path5;
|
|
14832
14832
|
return fn;
|
|
14833
14833
|
}
|
|
14834
14834
|
module.exports = function(fromModel) {
|
|
@@ -15099,7 +15099,7 @@ var require_wrap_ansi = __commonJS((exports, module) => {
|
|
|
15099
15099
|
}
|
|
15100
15100
|
return words.slice(0, last).join(" ") + words.slice(last).join("");
|
|
15101
15101
|
};
|
|
15102
|
-
var
|
|
15102
|
+
var exec3 = (string, columns, options = {}) => {
|
|
15103
15103
|
if (options.trim !== false && string.trim() === "") {
|
|
15104
15104
|
return "";
|
|
15105
15105
|
}
|
|
@@ -15173,7 +15173,7 @@ var require_wrap_ansi = __commonJS((exports, module) => {
|
|
|
15173
15173
|
module.exports = (string, columns, options) => {
|
|
15174
15174
|
return String(string).normalize().replace(/\r\n/g, `
|
|
15175
15175
|
`).split(`
|
|
15176
|
-
`).map((line) =>
|
|
15176
|
+
`).map((line) => exec3(line, columns, options)).join(`
|
|
15177
15177
|
`);
|
|
15178
15178
|
};
|
|
15179
15179
|
});
|
|
@@ -24156,16 +24156,16 @@ var require_os_tmpdir = __commonJS((exports, module) => {
|
|
|
24156
24156
|
var isWindows = process.platform === "win32";
|
|
24157
24157
|
var trailingSlashRe = isWindows ? /[^:]\\$/ : /.\/$/;
|
|
24158
24158
|
module.exports = function() {
|
|
24159
|
-
var
|
|
24159
|
+
var path5;
|
|
24160
24160
|
if (isWindows) {
|
|
24161
|
-
|
|
24161
|
+
path5 = process.env.TEMP || process.env.TMP || (process.env.SystemRoot || process.env.windir) + "\\temp";
|
|
24162
24162
|
} else {
|
|
24163
|
-
|
|
24163
|
+
path5 = process.env.TMPDIR || process.env.TMP || process.env.TEMP || "/tmp";
|
|
24164
24164
|
}
|
|
24165
|
-
if (trailingSlashRe.test(
|
|
24166
|
-
|
|
24165
|
+
if (trailingSlashRe.test(path5)) {
|
|
24166
|
+
path5 = path5.slice(0, -1);
|
|
24167
24167
|
}
|
|
24168
|
-
return
|
|
24168
|
+
return path5;
|
|
24169
24169
|
};
|
|
24170
24170
|
});
|
|
24171
24171
|
|
|
@@ -24179,7 +24179,7 @@ var require_tmp = __commonJS((exports, module) => {
|
|
|
24179
24179
|
* MIT Licensed
|
|
24180
24180
|
*/
|
|
24181
24181
|
var fs = __require("fs");
|
|
24182
|
-
var
|
|
24182
|
+
var path5 = __require("path");
|
|
24183
24183
|
var crypto2 = __require("crypto");
|
|
24184
24184
|
var osTmpDir = require_os_tmpdir();
|
|
24185
24185
|
var _c = process.binding("constants");
|
|
@@ -24221,7 +24221,7 @@ var require_tmp = __commonJS((exports, module) => {
|
|
|
24221
24221
|
}
|
|
24222
24222
|
function _generateTmpName(opts) {
|
|
24223
24223
|
if (opts.name) {
|
|
24224
|
-
return
|
|
24224
|
+
return path5.join(opts.dir || tmpDir, opts.name);
|
|
24225
24225
|
}
|
|
24226
24226
|
if (opts.template) {
|
|
24227
24227
|
return opts.template.replace(TEMPLATE_PATTERN, _randomChars(6));
|
|
@@ -24232,7 +24232,7 @@ var require_tmp = __commonJS((exports, module) => {
|
|
|
24232
24232
|
_randomChars(12),
|
|
24233
24233
|
opts.postfix || ""
|
|
24234
24234
|
].join("");
|
|
24235
|
-
return
|
|
24235
|
+
return path5.join(opts.dir || tmpDir, name);
|
|
24236
24236
|
}
|
|
24237
24237
|
function tmpName(options, callback) {
|
|
24238
24238
|
var args = _parseArguments(options, callback), opts = args[0], cb = args[1], tries = opts.name ? 1 : opts.tries || DEFAULT_TRIES;
|
|
@@ -24320,7 +24320,7 @@ var require_tmp = __commonJS((exports, module) => {
|
|
|
24320
24320
|
do {
|
|
24321
24321
|
var dir2 = dirs.pop(), deferred = false, files = fs.readdirSync(dir2);
|
|
24322
24322
|
for (var i = 0, length = files.length;i < length; i++) {
|
|
24323
|
-
var file2 =
|
|
24323
|
+
var file2 = path5.join(dir2, files[i]), stat = fs.lstatSync(file2);
|
|
24324
24324
|
if (stat.isDirectory()) {
|
|
24325
24325
|
if (!deferred) {
|
|
24326
24326
|
deferred = true;
|
|
@@ -34338,6 +34338,13 @@ var chalk = createChalk();
|
|
|
34338
34338
|
var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
34339
34339
|
var source_default = chalk;
|
|
34340
34340
|
|
|
34341
|
+
// src/lib/ai.ts
|
|
34342
|
+
import { exec } from "child_process";
|
|
34343
|
+
import { promisify } from "util";
|
|
34344
|
+
import { mkdtemp, readFile as readFile2, rm } from "fs/promises";
|
|
34345
|
+
import { tmpdir } from "os";
|
|
34346
|
+
import path4 from "path";
|
|
34347
|
+
|
|
34341
34348
|
// src/lib/env.ts
|
|
34342
34349
|
import { readFile, writeFile, mkdir, chmod } from "fs/promises";
|
|
34343
34350
|
import { existsSync } from "fs";
|
|
@@ -34420,7 +34427,7 @@ async function getResolvedEnv() {
|
|
|
34420
34427
|
return { value: global.geminiApiKey, source: "global-config" };
|
|
34421
34428
|
return { value: null, source: null };
|
|
34422
34429
|
})();
|
|
34423
|
-
const validProviders = ["anthropic", "openai", "gemini"];
|
|
34430
|
+
const validProviders = ["anthropic", "openai", "gemini", "claude-code", "codex"];
|
|
34424
34431
|
const provider = validProviders.includes(rawProvider.value) ? rawProvider.value : null;
|
|
34425
34432
|
_resolved = {
|
|
34426
34433
|
provider,
|
|
@@ -34453,6 +34460,7 @@ function formatSource(source) {
|
|
|
34453
34460
|
}
|
|
34454
34461
|
|
|
34455
34462
|
// src/lib/ai.ts
|
|
34463
|
+
var execAsync = promisify(exec);
|
|
34456
34464
|
async function makeAnthropicProvider(apiKey) {
|
|
34457
34465
|
const { default: Anthropic2 } = await Promise.resolve().then(() => (init_sdk(), exports_sdk));
|
|
34458
34466
|
const client = new Anthropic2({ apiKey });
|
|
@@ -34517,6 +34525,70 @@ async function makeGeminiProvider(apiKey) {
|
|
|
34517
34525
|
}
|
|
34518
34526
|
};
|
|
34519
34527
|
}
|
|
34528
|
+
async function makeClaudeCodeProvider() {
|
|
34529
|
+
try {
|
|
34530
|
+
await execAsync("claude --version", { timeout: 5000 });
|
|
34531
|
+
} catch {
|
|
34532
|
+
throw new Error("claude CLI not found — install Claude Code first");
|
|
34533
|
+
}
|
|
34534
|
+
return {
|
|
34535
|
+
name: "Claude Code",
|
|
34536
|
+
model: "claude (via CLI)",
|
|
34537
|
+
async complete(prompt) {
|
|
34538
|
+
const env3 = { ...process.env };
|
|
34539
|
+
delete env3.CLAUDECODE;
|
|
34540
|
+
const escaped = prompt.replace(/'/g, `'"'"'`);
|
|
34541
|
+
const { stdout, stderr } = await execAsync(`claude -p '${escaped}' --no-session-persistence --tools ""`, { env: env3, timeout: 60000 });
|
|
34542
|
+
const text = (stdout || stderr).trim();
|
|
34543
|
+
if (!text)
|
|
34544
|
+
throw new Error("Empty response from Claude Code CLI");
|
|
34545
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
34546
|
+
}
|
|
34547
|
+
};
|
|
34548
|
+
}
|
|
34549
|
+
async function makeCodexProvider() {
|
|
34550
|
+
try {
|
|
34551
|
+
await execAsync("codex --version", { timeout: 5000 });
|
|
34552
|
+
} catch {
|
|
34553
|
+
throw new Error("codex CLI not found — install OpenAI Codex first");
|
|
34554
|
+
}
|
|
34555
|
+
return {
|
|
34556
|
+
name: "Codex",
|
|
34557
|
+
model: "codex (via CLI)",
|
|
34558
|
+
async complete(prompt) {
|
|
34559
|
+
const tmpDir = await mkdtemp(path4.join(tmpdir(), "hermes-"));
|
|
34560
|
+
const promptFile = path4.join(tmpDir, "prompt.txt");
|
|
34561
|
+
const outputFile = path4.join(tmpDir, "output.txt");
|
|
34562
|
+
try {
|
|
34563
|
+
const { writeFile: writeFile2 } = await import("fs/promises");
|
|
34564
|
+
await writeFile2(promptFile, prompt, "utf-8");
|
|
34565
|
+
await execAsync(`codex exec --color never -o "${outputFile}" - < "${promptFile}"`, { timeout: 60000 });
|
|
34566
|
+
const text = (await readFile2(outputFile, "utf-8")).trim();
|
|
34567
|
+
if (!text)
|
|
34568
|
+
throw new Error("Empty response from Codex CLI");
|
|
34569
|
+
return text;
|
|
34570
|
+
} finally {
|
|
34571
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
34572
|
+
}
|
|
34573
|
+
}
|
|
34574
|
+
};
|
|
34575
|
+
}
|
|
34576
|
+
async function isCodexAvailable() {
|
|
34577
|
+
try {
|
|
34578
|
+
await execAsync("codex --version", { timeout: 3000 });
|
|
34579
|
+
return true;
|
|
34580
|
+
} catch {
|
|
34581
|
+
return false;
|
|
34582
|
+
}
|
|
34583
|
+
}
|
|
34584
|
+
async function isClaudeCodeAvailable() {
|
|
34585
|
+
try {
|
|
34586
|
+
await execAsync("claude --version", { timeout: 3000 });
|
|
34587
|
+
return true;
|
|
34588
|
+
} catch {
|
|
34589
|
+
return false;
|
|
34590
|
+
}
|
|
34591
|
+
}
|
|
34520
34592
|
var _provider = null;
|
|
34521
34593
|
async function getProvider() {
|
|
34522
34594
|
if (_provider)
|
|
@@ -34538,7 +34610,9 @@ async function getProvider() {
|
|
|
34538
34610
|
if (!env3.geminiApiKey)
|
|
34539
34611
|
throw new MissingKeyError("gemini");
|
|
34540
34612
|
return makeGeminiProvider(env3.geminiApiKey);
|
|
34541
|
-
}
|
|
34613
|
+
},
|
|
34614
|
+
"claude-code": () => makeClaudeCodeProvider(),
|
|
34615
|
+
codex: () => makeCodexProvider()
|
|
34542
34616
|
};
|
|
34543
34617
|
_provider = await factories[env3.provider]();
|
|
34544
34618
|
return _provider;
|
|
@@ -34555,6 +34629,14 @@ async function getProvider() {
|
|
|
34555
34629
|
_provider = await makeGeminiProvider(env3.geminiApiKey);
|
|
34556
34630
|
return _provider;
|
|
34557
34631
|
}
|
|
34632
|
+
if (await isClaudeCodeAvailable()) {
|
|
34633
|
+
_provider = await makeClaudeCodeProvider();
|
|
34634
|
+
return _provider;
|
|
34635
|
+
}
|
|
34636
|
+
if (await isCodexAvailable()) {
|
|
34637
|
+
_provider = await makeCodexProvider();
|
|
34638
|
+
return _provider;
|
|
34639
|
+
}
|
|
34558
34640
|
throw new NoProviderError;
|
|
34559
34641
|
}
|
|
34560
34642
|
|
|
@@ -34613,15 +34695,18 @@ class NoProviderError extends Error {
|
|
|
34613
34695
|
constructor() {
|
|
34614
34696
|
super([
|
|
34615
34697
|
"",
|
|
34616
|
-
source_default.red("
|
|
34698
|
+
source_default.red(" ✖ No AI provider configured"),
|
|
34617
34699
|
"",
|
|
34618
|
-
source_default.bold("
|
|
34700
|
+
source_default.bold(" Option 1 — use a local AI CLI (no API key needed):"),
|
|
34701
|
+
source_default.dim(" Claude Code: ") + source_default.cyan("hermes config set provider claude-code"),
|
|
34702
|
+
source_default.dim(" Codex CLI: ") + source_default.cyan("hermes config set provider codex"),
|
|
34619
34703
|
"",
|
|
34620
|
-
source_default.
|
|
34621
|
-
source_default.white("
|
|
34622
|
-
source_default.white("
|
|
34704
|
+
source_default.bold(" Option 2 — set an API key:"),
|
|
34705
|
+
source_default.white(" Anthropic ") + source_default.dim('export ANTHROPIC_API_KEY="sk-ant-..." → console.anthropic.com'),
|
|
34706
|
+
source_default.white(" OpenAI ") + source_default.dim('export OPENAI_API_KEY="sk-..." → platform.openai.com/api-keys'),
|
|
34707
|
+
source_default.white(" Gemini ") + source_default.dim('export GEMINI_API_KEY="AIza..." → aistudio.google.com/app/apikey'),
|
|
34623
34708
|
"",
|
|
34624
|
-
source_default.dim("Or
|
|
34709
|
+
source_default.dim(" Or run: hermes config setup"),
|
|
34625
34710
|
""
|
|
34626
34711
|
].join(`
|
|
34627
34712
|
`));
|
|
@@ -34671,6 +34756,24 @@ Ensure all Git commands are:
|
|
|
34671
34756
|
}
|
|
34672
34757
|
return response;
|
|
34673
34758
|
}
|
|
34759
|
+
async function getAllAvailableProviders() {
|
|
34760
|
+
const env3 = await getResolvedEnv();
|
|
34761
|
+
const providers = [];
|
|
34762
|
+
if (env3.anthropicApiKey) {
|
|
34763
|
+
providers.push(await makeAnthropicProvider(env3.anthropicApiKey));
|
|
34764
|
+
} else if (await isClaudeCodeAvailable()) {
|
|
34765
|
+
providers.push(await makeClaudeCodeProvider());
|
|
34766
|
+
}
|
|
34767
|
+
if (env3.openaiApiKey) {
|
|
34768
|
+
providers.push(await makeOpenAIProvider(env3.openaiApiKey));
|
|
34769
|
+
} else if (await isCodexAvailable()) {
|
|
34770
|
+
providers.push(await makeCodexProvider());
|
|
34771
|
+
}
|
|
34772
|
+
if (env3.geminiApiKey) {
|
|
34773
|
+
providers.push(await makeGeminiProvider(env3.geminiApiKey));
|
|
34774
|
+
}
|
|
34775
|
+
return providers;
|
|
34776
|
+
}
|
|
34674
34777
|
async function getActiveProvider() {
|
|
34675
34778
|
const provider = await getProvider();
|
|
34676
34779
|
return { name: provider.name, model: provider.model };
|
|
@@ -34697,9 +34800,9 @@ function stripMarkdownCodeBlock(text) {
|
|
|
34697
34800
|
}
|
|
34698
34801
|
|
|
34699
34802
|
// src/lib/git.ts
|
|
34700
|
-
import { exec } from "child_process";
|
|
34701
|
-
import { promisify } from "util";
|
|
34702
|
-
var
|
|
34803
|
+
import { exec as exec2 } from "child_process";
|
|
34804
|
+
import { promisify as promisify2 } from "util";
|
|
34805
|
+
var execAsync2 = promisify2(exec2);
|
|
34703
34806
|
async function getRepoState() {
|
|
34704
34807
|
try {
|
|
34705
34808
|
const [
|
|
@@ -34710,12 +34813,12 @@ async function getRepoState() {
|
|
|
34710
34813
|
mergeStatus,
|
|
34711
34814
|
cherryPickStatus
|
|
34712
34815
|
] = await Promise.all([
|
|
34713
|
-
|
|
34714
|
-
|
|
34715
|
-
|
|
34716
|
-
|
|
34717
|
-
|
|
34718
|
-
|
|
34816
|
+
execAsync2("git rev-parse --abbrev-ref HEAD").then((r) => r.stdout.trim()).catch(() => execAsync2("git symbolic-ref --short HEAD").then((r) => r.stdout.trim()).catch(() => "main")),
|
|
34817
|
+
execAsync2("git status --porcelain").then((r) => r.stdout.trim()),
|
|
34818
|
+
execAsync2("git rev-parse --abbrev-ref @{upstream}").then((r) => r.stdout.trim()).catch(() => null),
|
|
34819
|
+
execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -d ${r.stdout.trim()}/rebase-merge`)).then(() => true).catch(() => false),
|
|
34820
|
+
execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -f ${r.stdout.trim()}/MERGE_HEAD`)).then(() => true).catch(() => false),
|
|
34821
|
+
execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -f ${r.stdout.trim()}/CHERRY_PICK_HEAD`)).then(() => true).catch(() => false)
|
|
34719
34822
|
]);
|
|
34720
34823
|
const statusLines = status.split(`
|
|
34721
34824
|
`).filter((line) => line);
|
|
@@ -34724,7 +34827,7 @@ async function getRepoState() {
|
|
|
34724
34827
|
let ahead = 0;
|
|
34725
34828
|
let behind = 0;
|
|
34726
34829
|
if (tracking) {
|
|
34727
|
-
const aheadBehind = await
|
|
34830
|
+
const aheadBehind = await execAsync2(`git rev-list --left-right --count ${tracking}...HEAD`).then((r) => r.stdout.trim().split("\t")).catch(() => ["0", "0"]);
|
|
34728
34831
|
behind = parseInt(aheadBehind[0], 10);
|
|
34729
34832
|
ahead = parseInt(aheadBehind[1], 10);
|
|
34730
34833
|
}
|
|
@@ -34746,7 +34849,7 @@ async function getRepoState() {
|
|
|
34746
34849
|
}
|
|
34747
34850
|
async function isGitInstalled() {
|
|
34748
34851
|
try {
|
|
34749
|
-
await
|
|
34852
|
+
await execAsync2("git --version");
|
|
34750
34853
|
return true;
|
|
34751
34854
|
} catch {
|
|
34752
34855
|
return false;
|
|
@@ -34754,7 +34857,7 @@ async function isGitInstalled() {
|
|
|
34754
34857
|
}
|
|
34755
34858
|
async function isGitRepository() {
|
|
34756
34859
|
try {
|
|
34757
|
-
await
|
|
34860
|
+
await execAsync2("git rev-parse --git-dir");
|
|
34758
34861
|
return true;
|
|
34759
34862
|
} catch {
|
|
34760
34863
|
return false;
|
|
@@ -34762,7 +34865,7 @@ async function isGitRepository() {
|
|
|
34762
34865
|
}
|
|
34763
34866
|
async function hasCommits() {
|
|
34764
34867
|
try {
|
|
34765
|
-
await
|
|
34868
|
+
await execAsync2("git rev-parse HEAD");
|
|
34766
34869
|
return true;
|
|
34767
34870
|
} catch {
|
|
34768
34871
|
return false;
|
|
@@ -34770,7 +34873,7 @@ async function hasCommits() {
|
|
|
34770
34873
|
}
|
|
34771
34874
|
async function executeGitCommand(command) {
|
|
34772
34875
|
try {
|
|
34773
|
-
const { stdout, stderr } = await
|
|
34876
|
+
const { stdout, stderr } = await execAsync2(command);
|
|
34774
34877
|
return stdout || stderr;
|
|
34775
34878
|
} catch (error3) {
|
|
34776
34879
|
throw new Error(`Git command failed: ${error3.message}`);
|
|
@@ -34778,7 +34881,7 @@ async function executeGitCommand(command) {
|
|
|
34778
34881
|
}
|
|
34779
34882
|
async function getConflictedFiles() {
|
|
34780
34883
|
try {
|
|
34781
|
-
const { stdout } = await
|
|
34884
|
+
const { stdout } = await execAsync2("git diff --name-only --diff-filter=U");
|
|
34782
34885
|
return stdout.trim().split(`
|
|
34783
34886
|
`).filter((line) => line);
|
|
34784
34887
|
} catch {
|
|
@@ -34833,494 +34936,187 @@ function displayConflictExplanation(explanation, files) {
|
|
|
34833
34936
|
console.log(explanation);
|
|
34834
34937
|
}
|
|
34835
34938
|
|
|
34836
|
-
//
|
|
34837
|
-
|
|
34838
|
-
|
|
34839
|
-
|
|
34840
|
-
|
|
34841
|
-
|
|
34842
|
-
|
|
34843
|
-
|
|
34844
|
-
|
|
34845
|
-
|
|
34846
|
-
|
|
34847
|
-
|
|
34848
|
-
|
|
34849
|
-
|
|
34850
|
-
process.exit(1);
|
|
34851
|
-
}
|
|
34852
|
-
});
|
|
34853
|
-
}
|
|
34854
|
-
|
|
34855
|
-
// src/lib/config.ts
|
|
34856
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
34857
|
-
import { existsSync as existsSync2 } from "fs";
|
|
34858
|
-
async function loadConfig() {
|
|
34859
|
-
const configPath = ".hermes/config.json";
|
|
34860
|
-
if (!existsSync2(configPath)) {
|
|
34861
|
-
return null;
|
|
34862
|
-
}
|
|
34863
|
-
try {
|
|
34864
|
-
const content = await readFile2(configPath, "utf-8");
|
|
34865
|
-
return JSON.parse(content);
|
|
34866
|
-
} catch (error3) {
|
|
34867
|
-
console.warn("⚠️ Could not load .hermes/config.json");
|
|
34868
|
-
return null;
|
|
34939
|
+
// node_modules/@inquirer/core/dist/esm/lib/key.mjs
|
|
34940
|
+
var isUpKey = (key) => key.name === "up" || key.name === "k" || key.ctrl && key.name === "p";
|
|
34941
|
+
var isDownKey = (key) => key.name === "down" || key.name === "j" || key.ctrl && key.name === "n";
|
|
34942
|
+
var isSpaceKey = (key) => key.name === "space";
|
|
34943
|
+
var isBackspaceKey = (key) => key.name === "backspace";
|
|
34944
|
+
var isNumberKey = (key) => "123456789".includes(key.name);
|
|
34945
|
+
var isEnterKey = (key) => key.name === "enter" || key.name === "return";
|
|
34946
|
+
// node_modules/@inquirer/core/dist/esm/lib/errors.mjs
|
|
34947
|
+
class AbortPromptError extends Error {
|
|
34948
|
+
name = "AbortPromptError";
|
|
34949
|
+
message = "Prompt was aborted";
|
|
34950
|
+
constructor(options) {
|
|
34951
|
+
super();
|
|
34952
|
+
this.cause = options?.cause;
|
|
34869
34953
|
}
|
|
34870
34954
|
}
|
|
34871
|
-
|
|
34872
|
-
|
|
34873
|
-
|
|
34874
|
-
|
|
34875
|
-
branchName = branchName.replace(/\/{2,}/g, "/");
|
|
34876
|
-
branchName = branchName.replace(/\/$/, "");
|
|
34877
|
-
return branchName;
|
|
34955
|
+
|
|
34956
|
+
class CancelPromptError extends Error {
|
|
34957
|
+
name = "CancelPromptError";
|
|
34958
|
+
message = "Prompt was canceled";
|
|
34878
34959
|
}
|
|
34879
|
-
|
|
34880
|
-
|
|
34960
|
+
|
|
34961
|
+
class ExitPromptError extends Error {
|
|
34962
|
+
name = "ExitPromptError";
|
|
34881
34963
|
}
|
|
34882
34964
|
|
|
34883
|
-
|
|
34884
|
-
|
|
34885
|
-
import { existsSync as existsSync3 } from "fs";
|
|
34886
|
-
var STATS_FILE = ".hermes/stats.json";
|
|
34887
|
-
var MAX_HISTORY = 1000;
|
|
34888
|
-
async function loadStats() {
|
|
34889
|
-
if (!existsSync3(STATS_FILE)) {
|
|
34890
|
-
return createEmptyStats();
|
|
34891
|
-
}
|
|
34892
|
-
try {
|
|
34893
|
-
const content = await readFile3(STATS_FILE, "utf-8");
|
|
34894
|
-
return JSON.parse(content);
|
|
34895
|
-
} catch {
|
|
34896
|
-
return createEmptyStats();
|
|
34897
|
-
}
|
|
34965
|
+
class HookError extends Error {
|
|
34966
|
+
name = "HookError";
|
|
34898
34967
|
}
|
|
34899
|
-
|
|
34900
|
-
|
|
34901
|
-
|
|
34902
|
-
if (stats.commandHistory.length > MAX_HISTORY) {
|
|
34903
|
-
stats.commandHistory = stats.commandHistory.slice(-MAX_HISTORY);
|
|
34904
|
-
}
|
|
34905
|
-
await writeFile2(STATS_FILE, JSON.stringify(stats, null, 2));
|
|
34906
|
-
} catch (error3) {}
|
|
34968
|
+
|
|
34969
|
+
class ValidationError extends Error {
|
|
34970
|
+
name = "ValidationError";
|
|
34907
34971
|
}
|
|
34908
|
-
|
|
34909
|
-
|
|
34910
|
-
|
|
34911
|
-
|
|
34912
|
-
|
|
34913
|
-
|
|
34914
|
-
|
|
34915
|
-
|
|
34916
|
-
|
|
34972
|
+
// node_modules/@inquirer/core/dist/esm/lib/use-prefix.mjs
|
|
34973
|
+
import { AsyncResource as AsyncResource2 } from "node:async_hooks";
|
|
34974
|
+
|
|
34975
|
+
// node_modules/@inquirer/core/dist/esm/lib/hook-engine.mjs
|
|
34976
|
+
import { AsyncLocalStorage, AsyncResource } from "node:async_hooks";
|
|
34977
|
+
var hookStorage = new AsyncLocalStorage;
|
|
34978
|
+
function createStore(rl) {
|
|
34979
|
+
const store = {
|
|
34980
|
+
rl,
|
|
34981
|
+
hooks: [],
|
|
34982
|
+
hooksCleanup: [],
|
|
34983
|
+
hooksEffect: [],
|
|
34984
|
+
index: 0,
|
|
34985
|
+
handleChange() {}
|
|
34917
34986
|
};
|
|
34918
|
-
|
|
34919
|
-
stats.totalGitCommands += gitCommandsRun;
|
|
34920
|
-
stats.lastUsed = Date.now();
|
|
34921
|
-
stats.commandHistory.push(entry);
|
|
34922
|
-
await saveStats(stats);
|
|
34987
|
+
return store;
|
|
34923
34988
|
}
|
|
34924
|
-
|
|
34925
|
-
const
|
|
34926
|
-
|
|
34927
|
-
|
|
34928
|
-
|
|
34929
|
-
|
|
34930
|
-
|
|
34931
|
-
|
|
34932
|
-
|
|
34933
|
-
|
|
34989
|
+
function withHooks(rl, cb) {
|
|
34990
|
+
const store = createStore(rl);
|
|
34991
|
+
return hookStorage.run(store, () => {
|
|
34992
|
+
function cycle(render) {
|
|
34993
|
+
store.handleChange = () => {
|
|
34994
|
+
store.index = 0;
|
|
34995
|
+
render();
|
|
34996
|
+
};
|
|
34997
|
+
store.handleChange();
|
|
34998
|
+
}
|
|
34999
|
+
return cb(cycle);
|
|
34934
35000
|
});
|
|
34935
|
-
|
|
34936
|
-
|
|
34937
|
-
|
|
34938
|
-
|
|
34939
|
-
|
|
34940
|
-
|
|
34941
|
-
|
|
34942
|
-
|
|
34943
|
-
|
|
34944
|
-
|
|
34945
|
-
|
|
35001
|
+
}
|
|
35002
|
+
function getStore() {
|
|
35003
|
+
const store = hookStorage.getStore();
|
|
35004
|
+
if (!store) {
|
|
35005
|
+
throw new HookError("[Inquirer] Hook functions can only be called from within a prompt");
|
|
35006
|
+
}
|
|
35007
|
+
return store;
|
|
35008
|
+
}
|
|
35009
|
+
function readline() {
|
|
35010
|
+
return getStore().rl;
|
|
35011
|
+
}
|
|
35012
|
+
function withUpdates(fn) {
|
|
35013
|
+
const wrapped = (...args) => {
|
|
35014
|
+
const store = getStore();
|
|
35015
|
+
let shouldUpdate = false;
|
|
35016
|
+
const oldHandleChange = store.handleChange;
|
|
35017
|
+
store.handleChange = () => {
|
|
35018
|
+
shouldUpdate = true;
|
|
35019
|
+
};
|
|
35020
|
+
const returnValue = fn(...args);
|
|
35021
|
+
if (shouldUpdate) {
|
|
35022
|
+
oldHandleChange();
|
|
35023
|
+
}
|
|
35024
|
+
store.handleChange = oldHandleChange;
|
|
35025
|
+
return returnValue;
|
|
34946
35026
|
};
|
|
35027
|
+
return AsyncResource.bind(wrapped);
|
|
34947
35028
|
}
|
|
34948
|
-
function
|
|
34949
|
-
|
|
34950
|
-
|
|
34951
|
-
|
|
34952
|
-
|
|
34953
|
-
|
|
34954
|
-
|
|
35029
|
+
function withPointer(cb) {
|
|
35030
|
+
const store = getStore();
|
|
35031
|
+
const { index } = store;
|
|
35032
|
+
const pointer = {
|
|
35033
|
+
get() {
|
|
35034
|
+
return store.hooks[index];
|
|
35035
|
+
},
|
|
35036
|
+
set(value) {
|
|
35037
|
+
store.hooks[index] = value;
|
|
35038
|
+
},
|
|
35039
|
+
initialized: index in store.hooks
|
|
34955
35040
|
};
|
|
35041
|
+
const returnValue = cb(pointer);
|
|
35042
|
+
store.index++;
|
|
35043
|
+
return returnValue;
|
|
34956
35044
|
}
|
|
34957
|
-
|
|
34958
|
-
|
|
34959
|
-
function startCommand(program2) {
|
|
34960
|
-
program2.command("start").description("Start a new piece of work safely").argument("<task>", "Description of the task").action(async (task) => {
|
|
34961
|
-
const startTime = Date.now();
|
|
34962
|
-
let gitCommandsRun = 0;
|
|
34963
|
-
try {
|
|
34964
|
-
console.log(`\uD83D\uDE80 Starting new task...
|
|
34965
|
-
`);
|
|
34966
|
-
const config = await loadConfig();
|
|
34967
|
-
const repoState = await getRepoState();
|
|
34968
|
-
let suggestedBranchName;
|
|
34969
|
-
if (config) {
|
|
34970
|
-
suggestedBranchName = generateBranchName(config.branches.featurePattern, task);
|
|
34971
|
-
console.log(`\uD83D\uDCA1 Suggested branch: ${suggestedBranchName}
|
|
34972
|
-
`);
|
|
34973
|
-
}
|
|
34974
|
-
const planResponse = await getGitPlan(repoState, `Start working on: ${task}. ${suggestedBranchName ? `Suggested branch name: ${suggestedBranchName}.` : ""} Provide base branch, conventional branch name, and Git commands to create and switch to the branch.`);
|
|
34975
|
-
let plan;
|
|
34976
|
-
try {
|
|
34977
|
-
plan = JSON.parse(planResponse);
|
|
34978
|
-
} catch {
|
|
34979
|
-
console.log(`\uD83D\uDCAD Hermes suggests:
|
|
34980
|
-
`);
|
|
34981
|
-
console.log(planResponse);
|
|
34982
|
-
console.log(`
|
|
34983
|
-
⚠️ Could not auto-execute. Please review the plan above.`);
|
|
34984
|
-
return;
|
|
34985
|
-
}
|
|
34986
|
-
if (plan.baseBranch && plan.branchName) {
|
|
34987
|
-
console.log(`\uD83D\uDCCD Base branch: ${plan.baseBranch}`);
|
|
34988
|
-
console.log(`\uD83C\uDF3F New branch: ${plan.branchName}
|
|
34989
|
-
`);
|
|
34990
|
-
}
|
|
34991
|
-
if (plan.explanation) {
|
|
34992
|
-
console.log(`\uD83D\uDCAD ${plan.explanation}
|
|
34993
|
-
`);
|
|
34994
|
-
}
|
|
34995
|
-
if (plan.commands && Array.isArray(plan.commands)) {
|
|
34996
|
-
for (const command of plan.commands) {
|
|
34997
|
-
let cmdString;
|
|
34998
|
-
if (typeof command === "string") {
|
|
34999
|
-
cmdString = command;
|
|
35000
|
-
} else if (typeof command === "object" && command.command) {
|
|
35001
|
-
cmdString = command.command;
|
|
35002
|
-
} else if (typeof command === "object" && command.cmd) {
|
|
35003
|
-
cmdString = command.cmd;
|
|
35004
|
-
} else {
|
|
35005
|
-
console.warn("⚠️ Skipping invalid command:", command);
|
|
35006
|
-
continue;
|
|
35007
|
-
}
|
|
35008
|
-
validateGitCommand(cmdString);
|
|
35009
|
-
displayStep(cmdString);
|
|
35010
|
-
await executeGitCommand(cmdString);
|
|
35011
|
-
gitCommandsRun++;
|
|
35012
|
-
}
|
|
35013
|
-
const branchName = plan.branchName || "new branch";
|
|
35014
|
-
displaySuccess(`Successfully created and switched to ${branchName}`);
|
|
35015
|
-
if (config?.preferences.learningMode) {
|
|
35016
|
-
console.log(`
|
|
35017
|
-
\uD83D\uDCA1 Learning tip: Branch created from clean state ensures no unexpected commits`);
|
|
35018
|
-
}
|
|
35019
|
-
} else {
|
|
35020
|
-
console.log("⚠️ No commands to execute. See analysis above.");
|
|
35021
|
-
}
|
|
35022
|
-
const duration = (Date.now() - startTime) / 1000;
|
|
35023
|
-
await recordCommand("start", [task], duration, true, gitCommandsRun);
|
|
35024
|
-
} catch (error3) {
|
|
35025
|
-
const duration = (Date.now() - startTime) / 1000;
|
|
35026
|
-
await recordCommand("start", [task], duration, false, gitCommandsRun);
|
|
35027
|
-
console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
|
|
35028
|
-
process.exit(1);
|
|
35029
|
-
}
|
|
35030
|
-
});
|
|
35045
|
+
function handleChange() {
|
|
35046
|
+
getStore().handleChange();
|
|
35031
35047
|
}
|
|
35032
|
-
|
|
35033
|
-
|
|
35034
|
-
|
|
35035
|
-
|
|
35036
|
-
|
|
35037
|
-
|
|
35038
|
-
|
|
35039
|
-
|
|
35040
|
-
|
|
35041
|
-
const planResponse = await getGitPlan(repoState, `Save work in progress${messageNote}. Decide whether to commit or stash. Return JSON with: approach, commands[], explanation.`);
|
|
35042
|
-
let plan;
|
|
35043
|
-
try {
|
|
35044
|
-
plan = JSON.parse(planResponse);
|
|
35045
|
-
} catch {
|
|
35046
|
-
console.log(`\uD83D\uDCAD Hermes suggests:
|
|
35047
|
-
`);
|
|
35048
|
-
console.log(planResponse);
|
|
35049
|
-
console.log(`
|
|
35050
|
-
⚠️ Could not auto-execute. Please review the plan above.`);
|
|
35051
|
-
return;
|
|
35052
|
-
}
|
|
35053
|
-
if (plan.explanation) {
|
|
35054
|
-
console.log(`\uD83D\uDCAD ${plan.explanation}
|
|
35055
|
-
`);
|
|
35048
|
+
var effectScheduler = {
|
|
35049
|
+
queue(cb) {
|
|
35050
|
+
const store = getStore();
|
|
35051
|
+
const { index } = store;
|
|
35052
|
+
store.hooksEffect.push(() => {
|
|
35053
|
+
store.hooksCleanup[index]?.();
|
|
35054
|
+
const cleanFn = cb(readline());
|
|
35055
|
+
if (cleanFn != null && typeof cleanFn !== "function") {
|
|
35056
|
+
throw new ValidationError("useEffect return value must be a cleanup function or nothing.");
|
|
35056
35057
|
}
|
|
35057
|
-
|
|
35058
|
-
|
|
35059
|
-
|
|
35060
|
-
|
|
35061
|
-
|
|
35062
|
-
|
|
35063
|
-
|
|
35064
|
-
|
|
35065
|
-
|
|
35066
|
-
|
|
35067
|
-
|
|
35068
|
-
|
|
35069
|
-
|
|
35070
|
-
|
|
35071
|
-
|
|
35072
|
-
|
|
35073
|
-
|
|
35074
|
-
|
|
35075
|
-
|
|
35076
|
-
|
|
35077
|
-
|
|
35058
|
+
store.hooksCleanup[index] = cleanFn;
|
|
35059
|
+
});
|
|
35060
|
+
},
|
|
35061
|
+
run() {
|
|
35062
|
+
const store = getStore();
|
|
35063
|
+
withUpdates(() => {
|
|
35064
|
+
store.hooksEffect.forEach((effect) => {
|
|
35065
|
+
effect();
|
|
35066
|
+
});
|
|
35067
|
+
store.hooksEffect.length = 0;
|
|
35068
|
+
})();
|
|
35069
|
+
},
|
|
35070
|
+
clearAll() {
|
|
35071
|
+
const store = getStore();
|
|
35072
|
+
store.hooksCleanup.forEach((cleanFn) => {
|
|
35073
|
+
cleanFn?.();
|
|
35074
|
+
});
|
|
35075
|
+
store.hooksEffect.length = 0;
|
|
35076
|
+
store.hooksCleanup.length = 0;
|
|
35077
|
+
}
|
|
35078
|
+
};
|
|
35079
|
+
|
|
35080
|
+
// node_modules/@inquirer/core/dist/esm/lib/use-state.mjs
|
|
35081
|
+
function useState(defaultValue) {
|
|
35082
|
+
return withPointer((pointer) => {
|
|
35083
|
+
const setFn = (newValue) => {
|
|
35084
|
+
if (pointer.get() !== newValue) {
|
|
35085
|
+
pointer.set(newValue);
|
|
35086
|
+
handleChange();
|
|
35078
35087
|
}
|
|
35079
|
-
}
|
|
35080
|
-
|
|
35081
|
-
|
|
35088
|
+
};
|
|
35089
|
+
if (pointer.initialized) {
|
|
35090
|
+
return [pointer.get(), setFn];
|
|
35082
35091
|
}
|
|
35092
|
+
const value = typeof defaultValue === "function" ? defaultValue() : defaultValue;
|
|
35093
|
+
pointer.set(value);
|
|
35094
|
+
return [value, setFn];
|
|
35083
35095
|
});
|
|
35084
35096
|
}
|
|
35085
35097
|
|
|
35086
|
-
//
|
|
35087
|
-
function
|
|
35088
|
-
|
|
35089
|
-
|
|
35090
|
-
|
|
35091
|
-
|
|
35092
|
-
|
|
35093
|
-
const fromNote = options.from ? ` from ${options.from}` : " from main";
|
|
35094
|
-
const planResponse = await getGitPlan(repoState, `Sync branch${fromNote}. Evaluate if rebase or merge is safer. Check if branch is shared. Return JSON with: approach, isRisky, riskExplanation, commands[], explanation.`);
|
|
35095
|
-
let plan;
|
|
35096
|
-
try {
|
|
35097
|
-
plan = JSON.parse(planResponse);
|
|
35098
|
-
} catch {
|
|
35099
|
-
console.log(`\uD83D\uDCAD Hermes suggests:
|
|
35100
|
-
`);
|
|
35101
|
-
console.log(planResponse);
|
|
35102
|
-
console.log(`
|
|
35103
|
-
⚠️ Could not auto-execute. Please review the plan above.`);
|
|
35104
|
-
return;
|
|
35105
|
-
}
|
|
35106
|
-
if (plan.isRisky && plan.riskExplanation) {
|
|
35107
|
-
displayWarning(plan.riskExplanation);
|
|
35108
|
-
console.log();
|
|
35109
|
-
}
|
|
35110
|
-
if (plan.explanation) {
|
|
35111
|
-
console.log(`\uD83D\uDCAD ${plan.explanation}
|
|
35112
|
-
`);
|
|
35113
|
-
}
|
|
35114
|
-
if (plan.commands && Array.isArray(plan.commands)) {
|
|
35115
|
-
for (const command of plan.commands) {
|
|
35116
|
-
let cmdString;
|
|
35117
|
-
if (typeof command === "string") {
|
|
35118
|
-
cmdString = command;
|
|
35119
|
-
} else if (typeof command === "object" && command.command) {
|
|
35120
|
-
cmdString = command.command;
|
|
35121
|
-
} else if (typeof command === "object" && command.cmd) {
|
|
35122
|
-
cmdString = command.cmd;
|
|
35123
|
-
} else {
|
|
35124
|
-
console.warn("⚠️ Skipping invalid command:", command);
|
|
35125
|
-
continue;
|
|
35126
|
-
}
|
|
35127
|
-
validateGitCommand(cmdString);
|
|
35128
|
-
displayStep(cmdString);
|
|
35129
|
-
await executeGitCommand(cmdString);
|
|
35130
|
-
}
|
|
35131
|
-
const approach = plan.approach || "selected method";
|
|
35132
|
-
displaySuccess(`Branch synced using ${approach}`);
|
|
35133
|
-
} else {
|
|
35134
|
-
console.log("⚠️ No commands to execute.");
|
|
35135
|
-
}
|
|
35136
|
-
} catch (error3) {
|
|
35137
|
-
console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
|
|
35138
|
-
process.exit(1);
|
|
35098
|
+
// node_modules/@inquirer/core/dist/esm/lib/use-effect.mjs
|
|
35099
|
+
function useEffect(cb, depArray) {
|
|
35100
|
+
withPointer((pointer) => {
|
|
35101
|
+
const oldDeps = pointer.get();
|
|
35102
|
+
const hasChanged = !Array.isArray(oldDeps) || depArray.some((dep, i) => !Object.is(dep, oldDeps[i]));
|
|
35103
|
+
if (hasChanged) {
|
|
35104
|
+
effectScheduler.queue(cb);
|
|
35139
35105
|
}
|
|
35106
|
+
pointer.set(depArray);
|
|
35140
35107
|
});
|
|
35141
35108
|
}
|
|
35142
35109
|
|
|
35143
|
-
// node_modules/@inquirer/core/dist/esm/lib/
|
|
35144
|
-
var
|
|
35145
|
-
var isDownKey = (key) => key.name === "down" || key.name === "j" || key.ctrl && key.name === "n";
|
|
35146
|
-
var isSpaceKey = (key) => key.name === "space";
|
|
35147
|
-
var isBackspaceKey = (key) => key.name === "backspace";
|
|
35148
|
-
var isNumberKey = (key) => "123456789".includes(key.name);
|
|
35149
|
-
var isEnterKey = (key) => key.name === "enter" || key.name === "return";
|
|
35150
|
-
// node_modules/@inquirer/core/dist/esm/lib/errors.mjs
|
|
35151
|
-
class AbortPromptError extends Error {
|
|
35152
|
-
name = "AbortPromptError";
|
|
35153
|
-
message = "Prompt was aborted";
|
|
35154
|
-
constructor(options) {
|
|
35155
|
-
super();
|
|
35156
|
-
this.cause = options?.cause;
|
|
35157
|
-
}
|
|
35158
|
-
}
|
|
35159
|
-
|
|
35160
|
-
class CancelPromptError extends Error {
|
|
35161
|
-
name = "CancelPromptError";
|
|
35162
|
-
message = "Prompt was canceled";
|
|
35163
|
-
}
|
|
35110
|
+
// node_modules/@inquirer/core/dist/esm/lib/theme.mjs
|
|
35111
|
+
var import_yoctocolors_cjs = __toESM(require_yoctocolors_cjs(), 1);
|
|
35164
35112
|
|
|
35165
|
-
|
|
35166
|
-
|
|
35167
|
-
|
|
35168
|
-
|
|
35169
|
-
|
|
35170
|
-
|
|
35171
|
-
}
|
|
35172
|
-
|
|
35173
|
-
class ValidationError extends Error {
|
|
35174
|
-
name = "ValidationError";
|
|
35175
|
-
}
|
|
35176
|
-
// node_modules/@inquirer/core/dist/esm/lib/use-prefix.mjs
|
|
35177
|
-
import { AsyncResource as AsyncResource2 } from "node:async_hooks";
|
|
35178
|
-
|
|
35179
|
-
// node_modules/@inquirer/core/dist/esm/lib/hook-engine.mjs
|
|
35180
|
-
import { AsyncLocalStorage, AsyncResource } from "node:async_hooks";
|
|
35181
|
-
var hookStorage = new AsyncLocalStorage;
|
|
35182
|
-
function createStore(rl) {
|
|
35183
|
-
const store = {
|
|
35184
|
-
rl,
|
|
35185
|
-
hooks: [],
|
|
35186
|
-
hooksCleanup: [],
|
|
35187
|
-
hooksEffect: [],
|
|
35188
|
-
index: 0,
|
|
35189
|
-
handleChange() {}
|
|
35190
|
-
};
|
|
35191
|
-
return store;
|
|
35192
|
-
}
|
|
35193
|
-
function withHooks(rl, cb) {
|
|
35194
|
-
const store = createStore(rl);
|
|
35195
|
-
return hookStorage.run(store, () => {
|
|
35196
|
-
function cycle(render) {
|
|
35197
|
-
store.handleChange = () => {
|
|
35198
|
-
store.index = 0;
|
|
35199
|
-
render();
|
|
35200
|
-
};
|
|
35201
|
-
store.handleChange();
|
|
35202
|
-
}
|
|
35203
|
-
return cb(cycle);
|
|
35204
|
-
});
|
|
35205
|
-
}
|
|
35206
|
-
function getStore() {
|
|
35207
|
-
const store = hookStorage.getStore();
|
|
35208
|
-
if (!store) {
|
|
35209
|
-
throw new HookError("[Inquirer] Hook functions can only be called from within a prompt");
|
|
35210
|
-
}
|
|
35211
|
-
return store;
|
|
35212
|
-
}
|
|
35213
|
-
function readline() {
|
|
35214
|
-
return getStore().rl;
|
|
35215
|
-
}
|
|
35216
|
-
function withUpdates(fn) {
|
|
35217
|
-
const wrapped = (...args) => {
|
|
35218
|
-
const store = getStore();
|
|
35219
|
-
let shouldUpdate = false;
|
|
35220
|
-
const oldHandleChange = store.handleChange;
|
|
35221
|
-
store.handleChange = () => {
|
|
35222
|
-
shouldUpdate = true;
|
|
35223
|
-
};
|
|
35224
|
-
const returnValue = fn(...args);
|
|
35225
|
-
if (shouldUpdate) {
|
|
35226
|
-
oldHandleChange();
|
|
35227
|
-
}
|
|
35228
|
-
store.handleChange = oldHandleChange;
|
|
35229
|
-
return returnValue;
|
|
35230
|
-
};
|
|
35231
|
-
return AsyncResource.bind(wrapped);
|
|
35232
|
-
}
|
|
35233
|
-
function withPointer(cb) {
|
|
35234
|
-
const store = getStore();
|
|
35235
|
-
const { index } = store;
|
|
35236
|
-
const pointer = {
|
|
35237
|
-
get() {
|
|
35238
|
-
return store.hooks[index];
|
|
35239
|
-
},
|
|
35240
|
-
set(value) {
|
|
35241
|
-
store.hooks[index] = value;
|
|
35242
|
-
},
|
|
35243
|
-
initialized: index in store.hooks
|
|
35244
|
-
};
|
|
35245
|
-
const returnValue = cb(pointer);
|
|
35246
|
-
store.index++;
|
|
35247
|
-
return returnValue;
|
|
35248
|
-
}
|
|
35249
|
-
function handleChange() {
|
|
35250
|
-
getStore().handleChange();
|
|
35251
|
-
}
|
|
35252
|
-
var effectScheduler = {
|
|
35253
|
-
queue(cb) {
|
|
35254
|
-
const store = getStore();
|
|
35255
|
-
const { index } = store;
|
|
35256
|
-
store.hooksEffect.push(() => {
|
|
35257
|
-
store.hooksCleanup[index]?.();
|
|
35258
|
-
const cleanFn = cb(readline());
|
|
35259
|
-
if (cleanFn != null && typeof cleanFn !== "function") {
|
|
35260
|
-
throw new ValidationError("useEffect return value must be a cleanup function or nothing.");
|
|
35261
|
-
}
|
|
35262
|
-
store.hooksCleanup[index] = cleanFn;
|
|
35263
|
-
});
|
|
35264
|
-
},
|
|
35265
|
-
run() {
|
|
35266
|
-
const store = getStore();
|
|
35267
|
-
withUpdates(() => {
|
|
35268
|
-
store.hooksEffect.forEach((effect) => {
|
|
35269
|
-
effect();
|
|
35270
|
-
});
|
|
35271
|
-
store.hooksEffect.length = 0;
|
|
35272
|
-
})();
|
|
35273
|
-
},
|
|
35274
|
-
clearAll() {
|
|
35275
|
-
const store = getStore();
|
|
35276
|
-
store.hooksCleanup.forEach((cleanFn) => {
|
|
35277
|
-
cleanFn?.();
|
|
35278
|
-
});
|
|
35279
|
-
store.hooksEffect.length = 0;
|
|
35280
|
-
store.hooksCleanup.length = 0;
|
|
35281
|
-
}
|
|
35282
|
-
};
|
|
35283
|
-
|
|
35284
|
-
// node_modules/@inquirer/core/dist/esm/lib/use-state.mjs
|
|
35285
|
-
function useState(defaultValue) {
|
|
35286
|
-
return withPointer((pointer) => {
|
|
35287
|
-
const setFn = (newValue) => {
|
|
35288
|
-
if (pointer.get() !== newValue) {
|
|
35289
|
-
pointer.set(newValue);
|
|
35290
|
-
handleChange();
|
|
35291
|
-
}
|
|
35292
|
-
};
|
|
35293
|
-
if (pointer.initialized) {
|
|
35294
|
-
return [pointer.get(), setFn];
|
|
35295
|
-
}
|
|
35296
|
-
const value = typeof defaultValue === "function" ? defaultValue() : defaultValue;
|
|
35297
|
-
pointer.set(value);
|
|
35298
|
-
return [value, setFn];
|
|
35299
|
-
});
|
|
35300
|
-
}
|
|
35301
|
-
|
|
35302
|
-
// node_modules/@inquirer/core/dist/esm/lib/use-effect.mjs
|
|
35303
|
-
function useEffect(cb, depArray) {
|
|
35304
|
-
withPointer((pointer) => {
|
|
35305
|
-
const oldDeps = pointer.get();
|
|
35306
|
-
const hasChanged = !Array.isArray(oldDeps) || depArray.some((dep, i) => !Object.is(dep, oldDeps[i]));
|
|
35307
|
-
if (hasChanged) {
|
|
35308
|
-
effectScheduler.queue(cb);
|
|
35309
|
-
}
|
|
35310
|
-
pointer.set(depArray);
|
|
35311
|
-
});
|
|
35312
|
-
}
|
|
35313
|
-
|
|
35314
|
-
// node_modules/@inquirer/core/dist/esm/lib/theme.mjs
|
|
35315
|
-
var import_yoctocolors_cjs = __toESM(require_yoctocolors_cjs(), 1);
|
|
35316
|
-
|
|
35317
|
-
// node_modules/@inquirer/figures/dist/esm/index.js
|
|
35318
|
-
import process3 from "node:process";
|
|
35319
|
-
function isUnicodeSupported() {
|
|
35320
|
-
if (process3.platform !== "win32") {
|
|
35321
|
-
return process3.env["TERM"] !== "linux";
|
|
35322
|
-
}
|
|
35323
|
-
return Boolean(process3.env["WT_SESSION"]) || Boolean(process3.env["TERMINUS_SUBLIME"]) || process3.env["ConEmuTask"] === "{cmd::Cmder}" || process3.env["TERM_PROGRAM"] === "Terminus-Sublime" || process3.env["TERM_PROGRAM"] === "vscode" || process3.env["TERM"] === "xterm-256color" || process3.env["TERM"] === "alacritty" || process3.env["TERMINAL_EMULATOR"] === "JetBrains-JediTerm";
|
|
35113
|
+
// node_modules/@inquirer/figures/dist/esm/index.js
|
|
35114
|
+
import process3 from "node:process";
|
|
35115
|
+
function isUnicodeSupported() {
|
|
35116
|
+
if (process3.platform !== "win32") {
|
|
35117
|
+
return process3.env["TERM"] !== "linux";
|
|
35118
|
+
}
|
|
35119
|
+
return Boolean(process3.env["WT_SESSION"]) || Boolean(process3.env["TERMINUS_SUBLIME"]) || process3.env["ConEmuTask"] === "{cmd::Cmder}" || process3.env["TERM_PROGRAM"] === "Terminus-Sublime" || process3.env["TERM_PROGRAM"] === "vscode" || process3.env["TERM"] === "xterm-256color" || process3.env["TERM"] === "alacritty" || process3.env["TERMINAL_EMULATOR"] === "JetBrains-JediTerm";
|
|
35324
35120
|
}
|
|
35325
35121
|
var common = {
|
|
35326
35122
|
circleQuestionMark: "(?)",
|
|
@@ -37131,9 +36927,9 @@ var import_mute_stream2 = __toESM(require_lib(), 1);
|
|
|
37131
36927
|
import readline3 from "node:readline";
|
|
37132
36928
|
var import_ansi_escapes5 = __toESM(require_ansi_escapes(), 1);
|
|
37133
36929
|
var _ = {
|
|
37134
|
-
set: (obj,
|
|
36930
|
+
set: (obj, path5 = "", value) => {
|
|
37135
36931
|
let pointer = obj;
|
|
37136
|
-
|
|
36932
|
+
path5.split(".").forEach((key2, index, arr) => {
|
|
37137
36933
|
if (key2 === "__proto__" || key2 === "constructor")
|
|
37138
36934
|
return;
|
|
37139
36935
|
if (index === arr.length - 1) {
|
|
@@ -37144,8 +36940,8 @@ var _ = {
|
|
|
37144
36940
|
pointer = pointer[key2];
|
|
37145
36941
|
});
|
|
37146
36942
|
},
|
|
37147
|
-
get: (obj,
|
|
37148
|
-
const travel = (regexp) => String.prototype.split.call(
|
|
36943
|
+
get: (obj, path5 = "", defaultValue) => {
|
|
36944
|
+
const travel = (regexp) => String.prototype.split.call(path5, regexp).filter(Boolean).reduce((res, key2) => res !== null && res !== undefined ? res[key2] : res, obj);
|
|
37149
36945
|
const result = travel(/[,[\]]+?/) || travel(/[,.[\]]+?/);
|
|
37150
36946
|
return result === undefined || result === obj ? defaultValue : result;
|
|
37151
36947
|
}
|
|
@@ -37383,8 +37179,537 @@ var inquirer = {
|
|
|
37383
37179
|
};
|
|
37384
37180
|
var esm_default12 = inquirer;
|
|
37385
37181
|
|
|
37182
|
+
// src/lib/council.ts
|
|
37183
|
+
async function broadcastToCouncil(prompt2) {
|
|
37184
|
+
const providers = await getAllAvailableProviders();
|
|
37185
|
+
if (providers.length === 0) {
|
|
37186
|
+
throw new Error("No AI providers available. Run hermes config setup.");
|
|
37187
|
+
}
|
|
37188
|
+
const results = await Promise.allSettled(providers.map(async (p) => {
|
|
37189
|
+
const response = await p.complete(prompt2);
|
|
37190
|
+
return { providerName: p.name, model: p.model, response };
|
|
37191
|
+
}));
|
|
37192
|
+
return results.map((r, i) => r.status === "fulfilled" ? r.value : { providerName: providers[i].name, model: providers[i].model, response: "", error: r.reason?.message ?? "Unknown error" });
|
|
37193
|
+
}
|
|
37194
|
+
async function synthesizePerspectives(originalPrompt, perspectives, userFeedback) {
|
|
37195
|
+
const valid = perspectives.filter((p) => !p.error);
|
|
37196
|
+
if (valid.length === 0)
|
|
37197
|
+
throw new Error("No valid perspectives to synthesize");
|
|
37198
|
+
if (valid.length === 1)
|
|
37199
|
+
return valid[0].response;
|
|
37200
|
+
const perspectiveBlock = valid.map((p) => `### ${p.providerName} (${p.model})
|
|
37201
|
+
${p.response}`).join(`
|
|
37202
|
+
|
|
37203
|
+
`);
|
|
37204
|
+
const feedbackLine = userFeedback ? `
|
|
37205
|
+
User feedback to incorporate: "${userFeedback}"
|
|
37206
|
+
` : "";
|
|
37207
|
+
const prompt2 = `You are synthesizing advice from multiple AI assistants into one clear, actionable recommendation.
|
|
37208
|
+
|
|
37209
|
+
Original request: "${originalPrompt}"
|
|
37210
|
+
${feedbackLine}
|
|
37211
|
+
Each assistant's perspective:
|
|
37212
|
+
|
|
37213
|
+
${perspectiveBlock}
|
|
37214
|
+
|
|
37215
|
+
Synthesize these into a single, concise, actionable response. Where they agree, state the consensus confidently. Where they differ, explain the trade-off briefly and make a clear recommendation. Do not attribute individual opinions.`;
|
|
37216
|
+
return getAISuggestion(prompt2);
|
|
37217
|
+
}
|
|
37218
|
+
async function runCouncilSession(prompt2, options = {}) {
|
|
37219
|
+
const session = { originalPrompt: prompt2, rounds: [] };
|
|
37220
|
+
console.log(`
|
|
37221
|
+
` + source_default.bold("Council") + (options.label ? source_default.dim(` — ${options.label}`) : "") + `
|
|
37222
|
+
`);
|
|
37223
|
+
let currentPrompt = prompt2;
|
|
37224
|
+
let round = 0;
|
|
37225
|
+
while (true) {
|
|
37226
|
+
round++;
|
|
37227
|
+
process.stdout.write(source_default.dim(` Consulting council${round > 1 ? ` (round ${round})` : ""}...`));
|
|
37228
|
+
const perspectives = await broadcastToCouncil(currentPrompt);
|
|
37229
|
+
session.rounds.push({ feedback: round > 1 ? currentPrompt : undefined, perspectives });
|
|
37230
|
+
process.stdout.write("\r" + " ".repeat(50) + "\r");
|
|
37231
|
+
displayPerspectives(perspectives);
|
|
37232
|
+
const validPerspectives = perspectives.filter((p) => !p.error);
|
|
37233
|
+
const choices = [];
|
|
37234
|
+
validPerspectives.forEach((p, i) => {
|
|
37235
|
+
choices.push({ name: `Accept [${i + 1}] ${p.providerName}`, value: `accept:${i}` });
|
|
37236
|
+
});
|
|
37237
|
+
if (validPerspectives.length > 1) {
|
|
37238
|
+
choices.push({ name: "Synthesize — have the council agree on one answer", value: "synthesize" });
|
|
37239
|
+
}
|
|
37240
|
+
choices.push({ name: "Refine — give feedback and re-ask the council", value: "refine" });
|
|
37241
|
+
choices.push({ name: "Follow-up — ask the council a new question", value: "followup" });
|
|
37242
|
+
choices.push({ name: "Exit — discard", value: "exit" });
|
|
37243
|
+
const { action } = await esm_default12.prompt([{
|
|
37244
|
+
type: "list",
|
|
37245
|
+
name: "action",
|
|
37246
|
+
message: "What would you like to do?",
|
|
37247
|
+
choices
|
|
37248
|
+
}]);
|
|
37249
|
+
if (action === "exit")
|
|
37250
|
+
return null;
|
|
37251
|
+
if (action.startsWith("accept:")) {
|
|
37252
|
+
const idx = parseInt(action.split(":")[1], 10);
|
|
37253
|
+
return validPerspectives[idx].response;
|
|
37254
|
+
}
|
|
37255
|
+
if (action === "synthesize") {
|
|
37256
|
+
process.stdout.write(source_default.dim(" Synthesizing...\r"));
|
|
37257
|
+
const synthesis = await synthesizePerspectives(prompt2, perspectives);
|
|
37258
|
+
process.stdout.write(" ".repeat(30) + "\r");
|
|
37259
|
+
console.log(`
|
|
37260
|
+
` + source_default.dim(" ─".repeat(27)));
|
|
37261
|
+
console.log(" " + source_default.bold.cyan("Synthesis"));
|
|
37262
|
+
console.log(source_default.dim(" ─".repeat(27)));
|
|
37263
|
+
wrapText(synthesis).forEach((l) => console.log(" " + l));
|
|
37264
|
+
console.log(source_default.dim(" ─".repeat(27)) + `
|
|
37265
|
+
`);
|
|
37266
|
+
const { synthAction } = await esm_default12.prompt([{
|
|
37267
|
+
type: "list",
|
|
37268
|
+
name: "synthAction",
|
|
37269
|
+
message: "Use this synthesis?",
|
|
37270
|
+
choices: [
|
|
37271
|
+
{ name: "Yes, accept", value: "accept" },
|
|
37272
|
+
{ name: "Refine further", value: "refine" },
|
|
37273
|
+
{ name: "Exit", value: "exit" }
|
|
37274
|
+
]
|
|
37275
|
+
}]);
|
|
37276
|
+
if (synthAction === "accept")
|
|
37277
|
+
return synthesis;
|
|
37278
|
+
if (synthAction === "exit")
|
|
37279
|
+
return null;
|
|
37280
|
+
}
|
|
37281
|
+
const isFollowUp = action === "followup";
|
|
37282
|
+
const { feedback } = await esm_default12.prompt([{
|
|
37283
|
+
type: "input",
|
|
37284
|
+
name: "feedback",
|
|
37285
|
+
message: isFollowUp ? "Your question:" : "Your feedback (what to change or add):",
|
|
37286
|
+
validate: (v) => v.trim().length > 0 || "Please enter something"
|
|
37287
|
+
}]);
|
|
37288
|
+
if (isFollowUp) {
|
|
37289
|
+
currentPrompt = feedback.trim();
|
|
37290
|
+
} else {
|
|
37291
|
+
const perspectiveContext = perspectives.filter((p) => !p.error).map((p) => `[${p.providerName}]: ${p.response.slice(0, 500)}`).join(`
|
|
37292
|
+
|
|
37293
|
+
`);
|
|
37294
|
+
currentPrompt = `Original request: "${prompt2}"
|
|
37295
|
+
|
|
37296
|
+
Previous perspectives from the council:
|
|
37297
|
+
${perspectiveContext}
|
|
37298
|
+
|
|
37299
|
+
User feedback: "${feedback.trim()}"
|
|
37300
|
+
|
|
37301
|
+
Respond with an improved answer that addresses the feedback.`;
|
|
37302
|
+
}
|
|
37303
|
+
console.log();
|
|
37304
|
+
}
|
|
37305
|
+
}
|
|
37306
|
+
function displayPerspectives(perspectives) {
|
|
37307
|
+
const valid = perspectives.filter((p) => !p.error);
|
|
37308
|
+
const failed = perspectives.filter((p) => p.error);
|
|
37309
|
+
perspectives.filter((p) => !p.error).forEach((p, i) => {
|
|
37310
|
+
console.log(source_default.dim(" ─".repeat(27)));
|
|
37311
|
+
console.log(` ${source_default.bold.cyan(`[${i + 1}]`)} ${source_default.bold(p.providerName)} ${source_default.dim(p.model)}`);
|
|
37312
|
+
console.log(source_default.dim(" ─".repeat(27)));
|
|
37313
|
+
wrapText(p.response).forEach((l) => console.log(" " + l));
|
|
37314
|
+
console.log();
|
|
37315
|
+
});
|
|
37316
|
+
if (failed.length > 0) {
|
|
37317
|
+
failed.forEach((p) => {
|
|
37318
|
+
console.log(source_default.dim(` [${p.providerName}] unavailable: ${p.error}`));
|
|
37319
|
+
});
|
|
37320
|
+
console.log();
|
|
37321
|
+
}
|
|
37322
|
+
if (valid.length === 0) {
|
|
37323
|
+
throw new Error("All council members failed to respond.");
|
|
37324
|
+
}
|
|
37325
|
+
}
|
|
37326
|
+
function wrapText(text, width = 72) {
|
|
37327
|
+
const lines2 = [];
|
|
37328
|
+
for (const paragraph of text.split(`
|
|
37329
|
+
`)) {
|
|
37330
|
+
if (paragraph.trim() === "") {
|
|
37331
|
+
lines2.push("");
|
|
37332
|
+
continue;
|
|
37333
|
+
}
|
|
37334
|
+
const words = paragraph.split(" ");
|
|
37335
|
+
let line = "";
|
|
37336
|
+
for (const word of words) {
|
|
37337
|
+
if ((line + " " + word).trim().length > width) {
|
|
37338
|
+
if (line)
|
|
37339
|
+
lines2.push(line);
|
|
37340
|
+
line = word;
|
|
37341
|
+
} else {
|
|
37342
|
+
line = line ? line + " " + word : word;
|
|
37343
|
+
}
|
|
37344
|
+
}
|
|
37345
|
+
if (line)
|
|
37346
|
+
lines2.push(line);
|
|
37347
|
+
}
|
|
37348
|
+
return lines2;
|
|
37349
|
+
}
|
|
37350
|
+
|
|
37351
|
+
// src/commands/plan.ts
|
|
37352
|
+
function planCommand(program2) {
|
|
37353
|
+
program2.command("plan").description("Analyze the current repository state and propose a safe Git plan").argument("<intent>", "What you want to achieve").option("--council", "Force council mode (all available providers)").option("--no-council", "Skip council mode, use single provider only").action(async (intent, options) => {
|
|
37354
|
+
try {
|
|
37355
|
+
const repoState = await getRepoState();
|
|
37356
|
+
const providers = await getAllAvailableProviders();
|
|
37357
|
+
const useCouncil = options.council === true ? true : options.council === false ? false : providers.length >= 2;
|
|
37358
|
+
if (useCouncil && providers.length >= 2) {
|
|
37359
|
+
const providerList = providers.map((p) => source_default.dim(`[${p.name}]`)).join(" ");
|
|
37360
|
+
console.log(`
|
|
37361
|
+
${providerList}
|
|
37362
|
+
`);
|
|
37363
|
+
const prompt2 = buildPlanPrompt(intent, repoState);
|
|
37364
|
+
const result = await runCouncilSession(prompt2, { label: intent });
|
|
37365
|
+
if (result) {
|
|
37366
|
+
displayPlan(result, repoState);
|
|
37367
|
+
console.log(source_default.dim(`
|
|
37368
|
+
No changes made. Review the plan above.
|
|
37369
|
+
`));
|
|
37370
|
+
} else {
|
|
37371
|
+
console.log(source_default.dim(`
|
|
37372
|
+
Exited without selecting a plan.
|
|
37373
|
+
`));
|
|
37374
|
+
}
|
|
37375
|
+
} else {
|
|
37376
|
+
const { name, model } = await getActiveProvider();
|
|
37377
|
+
console.log(`
|
|
37378
|
+
Analyzing... ${source_default.dim(`[${name} / ${model}]`)}
|
|
37379
|
+
`);
|
|
37380
|
+
const analysis = await analyzeGitState(repoState, intent);
|
|
37381
|
+
displayPlan(analysis, repoState);
|
|
37382
|
+
console.log(source_default.dim(`
|
|
37383
|
+
No changes made. Review the plan above.
|
|
37384
|
+
`));
|
|
37385
|
+
if (providers.length >= 2) {
|
|
37386
|
+
console.log(source_default.dim(` Tip: run with --council to get perspectives from all providers.
|
|
37387
|
+
`));
|
|
37388
|
+
}
|
|
37389
|
+
}
|
|
37390
|
+
} catch (error3) {
|
|
37391
|
+
console.error(source_default.red(`
|
|
37392
|
+
✖ ` + (error3 instanceof Error ? error3.message : error3)));
|
|
37393
|
+
process.exit(1);
|
|
37394
|
+
}
|
|
37395
|
+
});
|
|
37396
|
+
}
|
|
37397
|
+
function buildPlanPrompt(intent, repoState) {
|
|
37398
|
+
return `You are a Git expert. A developer wants to: "${intent}"
|
|
37399
|
+
|
|
37400
|
+
Current repository state:
|
|
37401
|
+
- Branch: ${repoState.currentBranch}
|
|
37402
|
+
- Clean: ${repoState.isClean}
|
|
37403
|
+
- Uncommitted changes: ${repoState.hasUncommittedChanges}
|
|
37404
|
+
- Untracked files: ${repoState.hasUntrackedFiles}
|
|
37405
|
+
- Ahead: ${repoState.ahead} commits, Behind: ${repoState.behind} commits
|
|
37406
|
+
- In rebase: ${repoState.isInRebase}, In merge: ${repoState.isInMerge}
|
|
37407
|
+
- Remote tracking: ${repoState.remoteTracking ?? "none"}
|
|
37408
|
+
|
|
37409
|
+
Provide a clear, safe Git plan with:
|
|
37410
|
+
1. Recommended approach and reasoning
|
|
37411
|
+
2. Step-by-step git commands (must start with "git ")
|
|
37412
|
+
3. Any risks or things to watch out for
|
|
37413
|
+
|
|
37414
|
+
Be specific and concise.`;
|
|
37415
|
+
}
|
|
37416
|
+
|
|
37417
|
+
// src/lib/config.ts
|
|
37418
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
37419
|
+
import { existsSync as existsSync2 } from "fs";
|
|
37420
|
+
async function loadConfig() {
|
|
37421
|
+
const configPath = ".hermes/config.json";
|
|
37422
|
+
if (!existsSync2(configPath)) {
|
|
37423
|
+
return null;
|
|
37424
|
+
}
|
|
37425
|
+
try {
|
|
37426
|
+
const content = await readFile3(configPath, "utf-8");
|
|
37427
|
+
return JSON.parse(content);
|
|
37428
|
+
} catch (error3) {
|
|
37429
|
+
console.warn("⚠️ Could not load .hermes/config.json");
|
|
37430
|
+
return null;
|
|
37431
|
+
}
|
|
37432
|
+
}
|
|
37433
|
+
function generateBranchName(pattern, description, ticket) {
|
|
37434
|
+
let branchName = pattern;
|
|
37435
|
+
branchName = branchName.replace("{description}", slugify(description));
|
|
37436
|
+
branchName = branchName.replace("{ticket}", ticket || "");
|
|
37437
|
+
branchName = branchName.replace(/\/{2,}/g, "/");
|
|
37438
|
+
branchName = branchName.replace(/\/$/, "");
|
|
37439
|
+
return branchName;
|
|
37440
|
+
}
|
|
37441
|
+
function slugify(text) {
|
|
37442
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
37443
|
+
}
|
|
37444
|
+
function isProtectedBranch(branch, config) {
|
|
37445
|
+
if (!config) {
|
|
37446
|
+
return ["main", "master", "production", "staging"].includes(branch);
|
|
37447
|
+
}
|
|
37448
|
+
return config.project.protectedBranches.includes(branch);
|
|
37449
|
+
}
|
|
37450
|
+
|
|
37451
|
+
// src/lib/stats.ts
|
|
37452
|
+
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
37453
|
+
import { existsSync as existsSync3 } from "fs";
|
|
37454
|
+
var STATS_FILE = ".hermes/stats.json";
|
|
37455
|
+
var MAX_HISTORY = 1000;
|
|
37456
|
+
async function loadStats() {
|
|
37457
|
+
if (!existsSync3(STATS_FILE)) {
|
|
37458
|
+
return createEmptyStats();
|
|
37459
|
+
}
|
|
37460
|
+
try {
|
|
37461
|
+
const content = await readFile4(STATS_FILE, "utf-8");
|
|
37462
|
+
return JSON.parse(content);
|
|
37463
|
+
} catch {
|
|
37464
|
+
return createEmptyStats();
|
|
37465
|
+
}
|
|
37466
|
+
}
|
|
37467
|
+
async function saveStats(stats) {
|
|
37468
|
+
try {
|
|
37469
|
+
await mkdir2(".hermes", { recursive: true });
|
|
37470
|
+
if (stats.commandHistory.length > MAX_HISTORY) {
|
|
37471
|
+
stats.commandHistory = stats.commandHistory.slice(-MAX_HISTORY);
|
|
37472
|
+
}
|
|
37473
|
+
await writeFile2(STATS_FILE, JSON.stringify(stats, null, 2));
|
|
37474
|
+
} catch (error3) {}
|
|
37475
|
+
}
|
|
37476
|
+
async function recordCommand(command, args, duration, success, gitCommandsRun = 0) {
|
|
37477
|
+
const stats = await loadStats();
|
|
37478
|
+
const entry = {
|
|
37479
|
+
timestamp: Date.now(),
|
|
37480
|
+
command,
|
|
37481
|
+
args,
|
|
37482
|
+
duration,
|
|
37483
|
+
success,
|
|
37484
|
+
gitCommandsRun
|
|
37485
|
+
};
|
|
37486
|
+
stats.totalCommands++;
|
|
37487
|
+
stats.totalGitCommands += gitCommandsRun;
|
|
37488
|
+
stats.lastUsed = Date.now();
|
|
37489
|
+
stats.commandHistory.push(entry);
|
|
37490
|
+
await saveStats(stats);
|
|
37491
|
+
}
|
|
37492
|
+
async function getStatsSummary(days = 30) {
|
|
37493
|
+
const stats = await loadStats();
|
|
37494
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
37495
|
+
const recentEntries = stats.commandHistory.filter((e) => e.timestamp >= cutoff);
|
|
37496
|
+
const commandCounts = {};
|
|
37497
|
+
let successCount = 0;
|
|
37498
|
+
recentEntries.forEach((entry) => {
|
|
37499
|
+
commandCounts[entry.command] = (commandCounts[entry.command] || 0) + 1;
|
|
37500
|
+
if (entry.success)
|
|
37501
|
+
successCount++;
|
|
37502
|
+
});
|
|
37503
|
+
const topCommands = Object.entries(commandCounts).sort(([, a], [, b]) => b - a).slice(0, 5).map(([cmd, count]) => ({ command: cmd, count }));
|
|
37504
|
+
const totalDays = Math.max(1, Math.ceil((Date.now() - stats.startDate) / (24 * 60 * 60 * 1000)));
|
|
37505
|
+
return {
|
|
37506
|
+
totalCommands: recentEntries.length,
|
|
37507
|
+
allTimeCommands: stats.totalCommands,
|
|
37508
|
+
gitCommandsRun: recentEntries.reduce((sum, e) => sum + e.gitCommandsRun, 0),
|
|
37509
|
+
allTimeGitCommands: stats.totalGitCommands,
|
|
37510
|
+
successRate: recentEntries.length ? successCount / recentEntries.length : 0,
|
|
37511
|
+
topCommands,
|
|
37512
|
+
daysActive: totalDays,
|
|
37513
|
+
commandsPerDay: stats.totalCommands / totalDays
|
|
37514
|
+
};
|
|
37515
|
+
}
|
|
37516
|
+
function createEmptyStats() {
|
|
37517
|
+
return {
|
|
37518
|
+
totalCommands: 0,
|
|
37519
|
+
totalGitCommands: 0,
|
|
37520
|
+
commandHistory: [],
|
|
37521
|
+
startDate: Date.now(),
|
|
37522
|
+
lastUsed: Date.now()
|
|
37523
|
+
};
|
|
37524
|
+
}
|
|
37525
|
+
|
|
37526
|
+
// src/commands/start.ts
|
|
37527
|
+
function startCommand(program2) {
|
|
37528
|
+
program2.command("start").description("Start a new piece of work safely").argument("<task>", "Description of the task").action(async (task) => {
|
|
37529
|
+
const startTime = Date.now();
|
|
37530
|
+
let gitCommandsRun = 0;
|
|
37531
|
+
try {
|
|
37532
|
+
console.log(`\uD83D\uDE80 Starting new task...
|
|
37533
|
+
`);
|
|
37534
|
+
const config = await loadConfig();
|
|
37535
|
+
const repoState = await getRepoState();
|
|
37536
|
+
let suggestedBranchName;
|
|
37537
|
+
if (config) {
|
|
37538
|
+
suggestedBranchName = generateBranchName(config.branches.featurePattern, task);
|
|
37539
|
+
console.log(`\uD83D\uDCA1 Suggested branch: ${suggestedBranchName}
|
|
37540
|
+
`);
|
|
37541
|
+
}
|
|
37542
|
+
const planResponse = await getGitPlan(repoState, `Start working on: ${task}. ${suggestedBranchName ? `Suggested branch name: ${suggestedBranchName}.` : ""} Provide base branch, conventional branch name, and Git commands to create and switch to the branch.`);
|
|
37543
|
+
let plan;
|
|
37544
|
+
try {
|
|
37545
|
+
plan = JSON.parse(planResponse);
|
|
37546
|
+
} catch {
|
|
37547
|
+
console.log(`\uD83D\uDCAD Hermes suggests:
|
|
37548
|
+
`);
|
|
37549
|
+
console.log(planResponse);
|
|
37550
|
+
console.log(`
|
|
37551
|
+
⚠️ Could not auto-execute. Please review the plan above.`);
|
|
37552
|
+
return;
|
|
37553
|
+
}
|
|
37554
|
+
if (plan.baseBranch && plan.branchName) {
|
|
37555
|
+
console.log(`\uD83D\uDCCD Base branch: ${plan.baseBranch}`);
|
|
37556
|
+
console.log(`\uD83C\uDF3F New branch: ${plan.branchName}
|
|
37557
|
+
`);
|
|
37558
|
+
}
|
|
37559
|
+
if (plan.explanation) {
|
|
37560
|
+
console.log(`\uD83D\uDCAD ${plan.explanation}
|
|
37561
|
+
`);
|
|
37562
|
+
}
|
|
37563
|
+
if (plan.commands && Array.isArray(plan.commands)) {
|
|
37564
|
+
for (const command of plan.commands) {
|
|
37565
|
+
let cmdString;
|
|
37566
|
+
if (typeof command === "string") {
|
|
37567
|
+
cmdString = command;
|
|
37568
|
+
} else if (typeof command === "object" && command.command) {
|
|
37569
|
+
cmdString = command.command;
|
|
37570
|
+
} else if (typeof command === "object" && command.cmd) {
|
|
37571
|
+
cmdString = command.cmd;
|
|
37572
|
+
} else {
|
|
37573
|
+
console.warn("⚠️ Skipping invalid command:", command);
|
|
37574
|
+
continue;
|
|
37575
|
+
}
|
|
37576
|
+
validateGitCommand(cmdString);
|
|
37577
|
+
displayStep(cmdString);
|
|
37578
|
+
await executeGitCommand(cmdString);
|
|
37579
|
+
gitCommandsRun++;
|
|
37580
|
+
}
|
|
37581
|
+
const branchName = plan.branchName || "new branch";
|
|
37582
|
+
displaySuccess(`Successfully created and switched to ${branchName}`);
|
|
37583
|
+
if (config?.preferences.learningMode) {
|
|
37584
|
+
console.log(`
|
|
37585
|
+
\uD83D\uDCA1 Learning tip: Branch created from clean state ensures no unexpected commits`);
|
|
37586
|
+
}
|
|
37587
|
+
} else {
|
|
37588
|
+
console.log("⚠️ No commands to execute. See analysis above.");
|
|
37589
|
+
}
|
|
37590
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
37591
|
+
await recordCommand("start", [task], duration, true, gitCommandsRun);
|
|
37592
|
+
} catch (error3) {
|
|
37593
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
37594
|
+
await recordCommand("start", [task], duration, false, gitCommandsRun);
|
|
37595
|
+
console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
|
|
37596
|
+
process.exit(1);
|
|
37597
|
+
}
|
|
37598
|
+
});
|
|
37599
|
+
}
|
|
37600
|
+
|
|
37601
|
+
// src/commands/wip.ts
|
|
37602
|
+
function wipCommand(program2) {
|
|
37603
|
+
program2.command("wip").description("Save work safely when things get messy").option("-m, --message <message>", "Custom WIP message").action(async (options) => {
|
|
37604
|
+
try {
|
|
37605
|
+
console.log(`\uD83D\uDCBE Saving work in progress...
|
|
37606
|
+
`);
|
|
37607
|
+
const repoState = await getRepoState();
|
|
37608
|
+
const messageNote = options.message ? ` with message: "${options.message}"` : "";
|
|
37609
|
+
const planResponse = await getGitPlan(repoState, `Save work in progress${messageNote}. Decide whether to commit or stash. Return JSON with: approach, commands[], explanation.`);
|
|
37610
|
+
let plan;
|
|
37611
|
+
try {
|
|
37612
|
+
plan = JSON.parse(planResponse);
|
|
37613
|
+
} catch {
|
|
37614
|
+
console.log(`\uD83D\uDCAD Hermes suggests:
|
|
37615
|
+
`);
|
|
37616
|
+
console.log(planResponse);
|
|
37617
|
+
console.log(`
|
|
37618
|
+
⚠️ Could not auto-execute. Please review the plan above.`);
|
|
37619
|
+
return;
|
|
37620
|
+
}
|
|
37621
|
+
if (plan.explanation) {
|
|
37622
|
+
console.log(`\uD83D\uDCAD ${plan.explanation}
|
|
37623
|
+
`);
|
|
37624
|
+
}
|
|
37625
|
+
if (plan.commands && Array.isArray(plan.commands)) {
|
|
37626
|
+
for (const command of plan.commands) {
|
|
37627
|
+
let cmdString;
|
|
37628
|
+
if (typeof command === "string") {
|
|
37629
|
+
cmdString = command;
|
|
37630
|
+
} else if (typeof command === "object" && command.command) {
|
|
37631
|
+
cmdString = command.command;
|
|
37632
|
+
} else if (typeof command === "object" && command.cmd) {
|
|
37633
|
+
cmdString = command.cmd;
|
|
37634
|
+
} else {
|
|
37635
|
+
console.warn("⚠️ Skipping invalid command:", command);
|
|
37636
|
+
continue;
|
|
37637
|
+
}
|
|
37638
|
+
validateGitCommand(cmdString);
|
|
37639
|
+
displayStep(cmdString);
|
|
37640
|
+
await executeGitCommand(cmdString);
|
|
37641
|
+
}
|
|
37642
|
+
const approach = plan.approach || "selected method";
|
|
37643
|
+
displaySuccess(`Work saved using ${approach}`);
|
|
37644
|
+
} else {
|
|
37645
|
+
console.log("⚠️ No commands to execute.");
|
|
37646
|
+
}
|
|
37647
|
+
} catch (error3) {
|
|
37648
|
+
console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
|
|
37649
|
+
process.exit(1);
|
|
37650
|
+
}
|
|
37651
|
+
});
|
|
37652
|
+
}
|
|
37653
|
+
|
|
37654
|
+
// src/commands/sync.ts
|
|
37655
|
+
function syncCommand(program2) {
|
|
37656
|
+
program2.command("sync").description("Bring your branch up to date safely").option("--from <branch>", "Source branch to sync from (default: main)").action(async (options) => {
|
|
37657
|
+
try {
|
|
37658
|
+
console.log(`\uD83D\uDD04 Syncing branch...
|
|
37659
|
+
`);
|
|
37660
|
+
const repoState = await getRepoState();
|
|
37661
|
+
const fromNote = options.from ? ` from ${options.from}` : " from main";
|
|
37662
|
+
const planResponse = await getGitPlan(repoState, `Sync branch${fromNote}. Evaluate if rebase or merge is safer. Check if branch is shared. Return JSON with: approach, isRisky, riskExplanation, commands[], explanation.`);
|
|
37663
|
+
let plan;
|
|
37664
|
+
try {
|
|
37665
|
+
plan = JSON.parse(planResponse);
|
|
37666
|
+
} catch {
|
|
37667
|
+
console.log(`\uD83D\uDCAD Hermes suggests:
|
|
37668
|
+
`);
|
|
37669
|
+
console.log(planResponse);
|
|
37670
|
+
console.log(`
|
|
37671
|
+
⚠️ Could not auto-execute. Please review the plan above.`);
|
|
37672
|
+
return;
|
|
37673
|
+
}
|
|
37674
|
+
if (plan.isRisky && plan.riskExplanation) {
|
|
37675
|
+
displayWarning(plan.riskExplanation);
|
|
37676
|
+
console.log();
|
|
37677
|
+
}
|
|
37678
|
+
if (plan.explanation) {
|
|
37679
|
+
console.log(`\uD83D\uDCAD ${plan.explanation}
|
|
37680
|
+
`);
|
|
37681
|
+
}
|
|
37682
|
+
if (plan.commands && Array.isArray(plan.commands)) {
|
|
37683
|
+
for (const command of plan.commands) {
|
|
37684
|
+
let cmdString;
|
|
37685
|
+
if (typeof command === "string") {
|
|
37686
|
+
cmdString = command;
|
|
37687
|
+
} else if (typeof command === "object" && command.command) {
|
|
37688
|
+
cmdString = command.command;
|
|
37689
|
+
} else if (typeof command === "object" && command.cmd) {
|
|
37690
|
+
cmdString = command.cmd;
|
|
37691
|
+
} else {
|
|
37692
|
+
console.warn("⚠️ Skipping invalid command:", command);
|
|
37693
|
+
continue;
|
|
37694
|
+
}
|
|
37695
|
+
validateGitCommand(cmdString);
|
|
37696
|
+
displayStep(cmdString);
|
|
37697
|
+
await executeGitCommand(cmdString);
|
|
37698
|
+
}
|
|
37699
|
+
const approach = plan.approach || "selected method";
|
|
37700
|
+
displaySuccess(`Branch synced using ${approach}`);
|
|
37701
|
+
} else {
|
|
37702
|
+
console.log("⚠️ No commands to execute.");
|
|
37703
|
+
}
|
|
37704
|
+
} catch (error3) {
|
|
37705
|
+
console.error("❌ Error:", error3 instanceof Error ? error3.message : error3);
|
|
37706
|
+
process.exit(1);
|
|
37707
|
+
}
|
|
37708
|
+
});
|
|
37709
|
+
}
|
|
37710
|
+
|
|
37386
37711
|
// src/commands/conflict.ts
|
|
37387
|
-
import { readFile as
|
|
37712
|
+
import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
37388
37713
|
function conflictCommand(program2) {
|
|
37389
37714
|
const conflict = program2.command("conflict").description("Understand and resolve merge conflicts");
|
|
37390
37715
|
conflict.command("explain").description("Understand why a conflict exists").action(async () => {
|
|
@@ -37418,7 +37743,7 @@ function conflictCommand(program2) {
|
|
|
37418
37743
|
\uD83D\uDCC4 ${file}`);
|
|
37419
37744
|
let fileContent;
|
|
37420
37745
|
try {
|
|
37421
|
-
fileContent = await
|
|
37746
|
+
fileContent = await readFile5(file, "utf-8");
|
|
37422
37747
|
} catch (error3) {
|
|
37423
37748
|
console.log(`⚠️ Could not read ${file}, skipping...`);
|
|
37424
37749
|
continue;
|
|
@@ -37526,8 +37851,8 @@ function worktreeCommand(program2) {
|
|
|
37526
37851
|
displayStep(cmdString);
|
|
37527
37852
|
await executeGitCommand(cmdString);
|
|
37528
37853
|
}
|
|
37529
|
-
const
|
|
37530
|
-
displaySuccess(`Worktree created at ${
|
|
37854
|
+
const path5 = plan.worktreePath || "new worktree";
|
|
37855
|
+
displaySuccess(`Worktree created at ${path5}`);
|
|
37531
37856
|
} else {
|
|
37532
37857
|
console.log("⚠️ No commands to execute.");
|
|
37533
37858
|
}
|
|
@@ -37541,10 +37866,10 @@ function worktreeCommand(program2) {
|
|
|
37541
37866
|
// src/commands/init.ts
|
|
37542
37867
|
import { writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
|
|
37543
37868
|
import { existsSync as existsSync4 } from "fs";
|
|
37544
|
-
import
|
|
37545
|
-
import { exec as
|
|
37546
|
-
import { promisify as
|
|
37547
|
-
var
|
|
37869
|
+
import path5 from "path";
|
|
37870
|
+
import { exec as exec3 } from "child_process";
|
|
37871
|
+
import { promisify as promisify3 } from "util";
|
|
37872
|
+
var execAsync3 = promisify3(exec3);
|
|
37548
37873
|
function initCommand(program2) {
|
|
37549
37874
|
program2.command("init").description("Initialize Hermes configuration for this repository").option("--quick", "Skip interactive prompts, use defaults").action(async (options) => {
|
|
37550
37875
|
try {
|
|
@@ -37561,7 +37886,7 @@ function initCommand(program2) {
|
|
|
37561
37886
|
`);
|
|
37562
37887
|
if (options.quick) {
|
|
37563
37888
|
console.log("Running: git init");
|
|
37564
|
-
await
|
|
37889
|
+
await execAsync3("git init");
|
|
37565
37890
|
console.log(`✓ Git repository initialized
|
|
37566
37891
|
`);
|
|
37567
37892
|
} else {
|
|
@@ -37577,7 +37902,7 @@ function initCommand(program2) {
|
|
|
37577
37902
|
console.log("Cancelled. Please run this command in a Git repository.");
|
|
37578
37903
|
process.exit(1);
|
|
37579
37904
|
}
|
|
37580
|
-
await
|
|
37905
|
+
await execAsync3("git init");
|
|
37581
37906
|
console.log(`✓ Git repository initialized
|
|
37582
37907
|
`);
|
|
37583
37908
|
}
|
|
@@ -37603,7 +37928,7 @@ function initCommand(program2) {
|
|
|
37603
37928
|
currentBranch = repoState.currentBranch;
|
|
37604
37929
|
} else {
|
|
37605
37930
|
try {
|
|
37606
|
-
const { stdout } = await
|
|
37931
|
+
const { stdout } = await execAsync3("git config --get init.defaultBranch");
|
|
37607
37932
|
currentBranch = stdout.trim() || "main";
|
|
37608
37933
|
} catch {
|
|
37609
37934
|
currentBranch = "main";
|
|
@@ -37655,7 +37980,7 @@ function createDefaultConfig(currentBranch) {
|
|
|
37655
37980
|
return {
|
|
37656
37981
|
version: "0.1.0",
|
|
37657
37982
|
project: {
|
|
37658
|
-
name:
|
|
37983
|
+
name: path5.basename(process.cwd()),
|
|
37659
37984
|
mainBranch: currentBranch === "master" ? "master" : "main",
|
|
37660
37985
|
protectedBranches: ["main", "master", "production", "staging"]
|
|
37661
37986
|
},
|
|
@@ -37687,7 +38012,7 @@ async function interactiveConfig(repoInfo) {
|
|
|
37687
38012
|
type: "input",
|
|
37688
38013
|
name: "projectName",
|
|
37689
38014
|
message: "Project name:",
|
|
37690
|
-
default:
|
|
38015
|
+
default: path5.basename(process.cwd())
|
|
37691
38016
|
},
|
|
37692
38017
|
{
|
|
37693
38018
|
type: "input",
|
|
@@ -37770,7 +38095,8 @@ var GITIGNORE_ENTRIES = [
|
|
|
37770
38095
|
{ pattern: ".env.*", comment: null },
|
|
37771
38096
|
{ pattern: "!.env.example", comment: null },
|
|
37772
38097
|
{ pattern: ".hermes/backups/", comment: null },
|
|
37773
|
-
{ pattern: ".hermes/stats.json", comment: null }
|
|
38098
|
+
{ pattern: ".hermes/stats.json", comment: null },
|
|
38099
|
+
{ pattern: ".hermes/plans/", comment: null }
|
|
37774
38100
|
];
|
|
37775
38101
|
async function updateGitignore() {
|
|
37776
38102
|
const { appendFile, readFile: rf } = await import("fs/promises");
|
|
@@ -37850,60 +38176,425 @@ function statsCommand(program2) {
|
|
|
37850
38176
|
}
|
|
37851
38177
|
|
|
37852
38178
|
// src/commands/workflow.ts
|
|
38179
|
+
import { exec as exec7 } from "child_process";
|
|
38180
|
+
import { promisify as promisify7 } from "util";
|
|
38181
|
+
|
|
38182
|
+
// src/lib/standup.ts
|
|
38183
|
+
import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
|
|
38184
|
+
import { existsSync as existsSync5 } from "fs";
|
|
38185
|
+
import { exec as exec4 } from "child_process";
|
|
38186
|
+
import { promisify as promisify4 } from "util";
|
|
38187
|
+
import path6 from "path";
|
|
38188
|
+
var execAsync4 = promisify4(exec4);
|
|
38189
|
+
var PLANS_DIR = ".hermes/plans";
|
|
38190
|
+
function toDateString(d) {
|
|
38191
|
+
return d.toISOString().slice(0, 10);
|
|
38192
|
+
}
|
|
38193
|
+
function today() {
|
|
38194
|
+
return toDateString(new Date);
|
|
38195
|
+
}
|
|
38196
|
+
function lastWorkday() {
|
|
38197
|
+
const d = new Date;
|
|
38198
|
+
const day = d.getDay();
|
|
38199
|
+
const daysBack = day === 1 ? 3 : day === 0 ? 2 : 1;
|
|
38200
|
+
d.setDate(d.getDate() - daysBack);
|
|
38201
|
+
return toDateString(d);
|
|
38202
|
+
}
|
|
38203
|
+
function planPath(date) {
|
|
38204
|
+
return path6.join(PLANS_DIR, `${date}.md`);
|
|
38205
|
+
}
|
|
38206
|
+
async function readPlan(date) {
|
|
38207
|
+
const file = planPath(date);
|
|
38208
|
+
if (!existsSync5(file))
|
|
38209
|
+
return null;
|
|
38210
|
+
try {
|
|
38211
|
+
const text = (await readFile6(file, "utf-8")).trim();
|
|
38212
|
+
return text || null;
|
|
38213
|
+
} catch {
|
|
38214
|
+
return null;
|
|
38215
|
+
}
|
|
38216
|
+
}
|
|
38217
|
+
async function writePlan(date, content) {
|
|
38218
|
+
await mkdir4(PLANS_DIR, { recursive: true });
|
|
38219
|
+
await writeFile5(planPath(date), content.trim() + `
|
|
38220
|
+
`);
|
|
38221
|
+
}
|
|
38222
|
+
function todayKey() {
|
|
38223
|
+
return today();
|
|
38224
|
+
}
|
|
38225
|
+
function lastWorkdayKey() {
|
|
38226
|
+
return lastWorkday();
|
|
38227
|
+
}
|
|
38228
|
+
async function getCommitsSince(sinceDate) {
|
|
38229
|
+
try {
|
|
38230
|
+
const authorEmail = await execAsync4("git config user.email").then((r) => r.stdout.trim()).catch(() => "");
|
|
38231
|
+
const format = "%H\x1F%s\x1F%as";
|
|
38232
|
+
const authorFlag = authorEmail ? `--author=${JSON.stringify(authorEmail)}` : "";
|
|
38233
|
+
const { stdout } = await execAsync4(`git log --since="${sinceDate} 00:00" --format="${format}" ${authorFlag} --no-merges`);
|
|
38234
|
+
return stdout.trim().split(`
|
|
38235
|
+
`).filter(Boolean).map((line) => {
|
|
38236
|
+
const [hash, subject, date] = line.split("\x1F");
|
|
38237
|
+
return { hash: hash.slice(0, 7), subject, date };
|
|
38238
|
+
});
|
|
38239
|
+
} catch {
|
|
38240
|
+
return [];
|
|
38241
|
+
}
|
|
38242
|
+
}
|
|
38243
|
+
async function detectBlockers(repoState) {
|
|
38244
|
+
const blockers = [];
|
|
38245
|
+
if (repoState.isInRebase) {
|
|
38246
|
+
blockers.push({ severity: "block", message: "Rebase in progress — resolve before continuing" });
|
|
38247
|
+
}
|
|
38248
|
+
if (repoState.isInMerge) {
|
|
38249
|
+
blockers.push({ severity: "block", message: "Merge in progress — resolve conflicts" });
|
|
38250
|
+
}
|
|
38251
|
+
if (repoState.isInCherryPick) {
|
|
38252
|
+
blockers.push({ severity: "block", message: "Cherry-pick in progress" });
|
|
38253
|
+
}
|
|
38254
|
+
if (repoState.behind > 0) {
|
|
38255
|
+
blockers.push({
|
|
38256
|
+
severity: "warn",
|
|
38257
|
+
message: `Branch is ${repoState.behind} commit${repoState.behind !== 1 ? "s" : ""} behind remote — run hermes sync`
|
|
38258
|
+
});
|
|
38259
|
+
}
|
|
38260
|
+
if (repoState.hasUncommittedChanges) {
|
|
38261
|
+
blockers.push({ severity: "warn", message: "Uncommitted changes in working tree" });
|
|
38262
|
+
}
|
|
38263
|
+
try {
|
|
38264
|
+
const { stdout } = await execAsync4(`git log --format="%as" ${repoState.currentBranch} --not --remotes=origin/main --not --remotes=origin/master -- 2>/dev/null | tail -1`);
|
|
38265
|
+
const oldest = stdout.trim();
|
|
38266
|
+
if (oldest) {
|
|
38267
|
+
const daysOld = Math.floor((Date.now() - new Date(oldest).getTime()) / 86400000);
|
|
38268
|
+
if (daysOld >= 5) {
|
|
38269
|
+
blockers.push({
|
|
38270
|
+
severity: "warn",
|
|
38271
|
+
message: `Branch has unmerged work going back ${daysOld} days — consider opening a PR`
|
|
38272
|
+
});
|
|
38273
|
+
}
|
|
38274
|
+
}
|
|
38275
|
+
} catch {}
|
|
38276
|
+
return blockers;
|
|
38277
|
+
}
|
|
38278
|
+
async function suggestTodayPlan(ctx) {
|
|
38279
|
+
const prompt2 = `You are helping a developer plan their day based on their git activity.
|
|
38280
|
+
|
|
38281
|
+
Suggest a concise, practical focus for today in 1-2 sentences.
|
|
38282
|
+
Be specific — reference the actual work in progress from the branch name and commits.
|
|
38283
|
+
Do NOT use bullet points or markdown. Return plain text only.
|
|
38284
|
+
|
|
38285
|
+
Context:
|
|
38286
|
+
- Current branch: ${ctx.currentBranch}
|
|
38287
|
+
- Yesterday's plan: ${ctx.yesterdayPlan ?? "(not recorded)"}
|
|
38288
|
+
- Yesterday's commits: ${ctx.yesterdayCommits.length > 0 ? ctx.yesterdayCommits.map((c) => c.subject).join(", ") : "none"}
|
|
38289
|
+
- Today's commits so far: ${ctx.todayCommits.length > 0 ? ctx.todayCommits.map((c) => c.subject).join(", ") : "none"}
|
|
38290
|
+
- Uncommitted work in progress: ${ctx.hasUncommittedChanges ? "yes" : "no"}
|
|
38291
|
+
- Blockers: ${ctx.blockers.length > 0 ? ctx.blockers.map((b) => b.message).join("; ") : "none"}
|
|
38292
|
+
|
|
38293
|
+
Suggest what to focus on today:`;
|
|
38294
|
+
return getAISuggestion(prompt2);
|
|
38295
|
+
}
|
|
38296
|
+
|
|
38297
|
+
// src/lib/integrations/claude-code.ts
|
|
38298
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
38299
|
+
import { existsSync as existsSync6 } from "fs";
|
|
38300
|
+
import { exec as exec5 } from "child_process";
|
|
38301
|
+
import { promisify as promisify5 } from "util";
|
|
38302
|
+
import { homedir as homedir2 } from "os";
|
|
38303
|
+
import path7 from "path";
|
|
38304
|
+
var execAsync5 = promisify5(exec5);
|
|
38305
|
+
function pathToProjectKey(absPath) {
|
|
38306
|
+
return absPath.replace(/\//g, "-");
|
|
38307
|
+
}
|
|
38308
|
+
async function getProjectSessionIds() {
|
|
38309
|
+
const projectKey = pathToProjectKey(process.cwd());
|
|
38310
|
+
const projectDir = path7.join(homedir2(), ".claude", "projects", projectKey);
|
|
38311
|
+
if (!existsSync6(projectDir))
|
|
38312
|
+
return [];
|
|
38313
|
+
try {
|
|
38314
|
+
const { stdout } = await execAsync5(`ls -1 "${projectDir}"`);
|
|
38315
|
+
return stdout.trim().split(`
|
|
38316
|
+
`).filter((name) => name && !name.endsWith(".jsonl"));
|
|
38317
|
+
} catch {
|
|
38318
|
+
return [];
|
|
38319
|
+
}
|
|
38320
|
+
}
|
|
38321
|
+
async function getClaudeCodeTodos() {
|
|
38322
|
+
const sessionIds = await getProjectSessionIds();
|
|
38323
|
+
if (sessionIds.length === 0)
|
|
38324
|
+
return [];
|
|
38325
|
+
const todosDir = path7.join(homedir2(), ".claude", "todos");
|
|
38326
|
+
if (!existsSync6(todosDir))
|
|
38327
|
+
return [];
|
|
38328
|
+
const todos = [];
|
|
38329
|
+
for (const sessionId of sessionIds) {
|
|
38330
|
+
try {
|
|
38331
|
+
const { stdout: files } = await execAsync5(`ls -1 "${todosDir}" | grep "^${sessionId}"`);
|
|
38332
|
+
for (const file of files.trim().split(`
|
|
38333
|
+
`).filter(Boolean)) {
|
|
38334
|
+
try {
|
|
38335
|
+
const raw = await readFile7(path7.join(todosDir, file), "utf-8");
|
|
38336
|
+
const parsed = JSON.parse(raw);
|
|
38337
|
+
if (Array.isArray(parsed)) {
|
|
38338
|
+
for (const todo of parsed) {
|
|
38339
|
+
if (todo.status === "pending" || todo.status === "in_progress") {
|
|
38340
|
+
todos.push(todo);
|
|
38341
|
+
}
|
|
38342
|
+
}
|
|
38343
|
+
}
|
|
38344
|
+
} catch {}
|
|
38345
|
+
}
|
|
38346
|
+
} catch {}
|
|
38347
|
+
}
|
|
38348
|
+
const seen = new Set;
|
|
38349
|
+
return todos.filter((t) => {
|
|
38350
|
+
if (seen.has(t.content))
|
|
38351
|
+
return false;
|
|
38352
|
+
seen.add(t.content);
|
|
38353
|
+
return true;
|
|
38354
|
+
});
|
|
38355
|
+
}
|
|
38356
|
+
|
|
38357
|
+
// src/lib/integrations/github.ts
|
|
38358
|
+
import { exec as exec6 } from "child_process";
|
|
38359
|
+
import { promisify as promisify6 } from "util";
|
|
38360
|
+
var execAsync6 = promisify6(exec6);
|
|
38361
|
+
async function getAssignedIssues() {
|
|
38362
|
+
try {
|
|
38363
|
+
await execAsync6("gh --version");
|
|
38364
|
+
} catch {
|
|
38365
|
+
return null;
|
|
38366
|
+
}
|
|
38367
|
+
try {
|
|
38368
|
+
const { stdout } = await execAsync6("gh issue list --assignee @me --state open --limit 20 --json number,title,url,labels");
|
|
38369
|
+
const raw = JSON.parse(stdout);
|
|
38370
|
+
return raw.map((i) => ({
|
|
38371
|
+
number: i.number,
|
|
38372
|
+
title: i.title,
|
|
38373
|
+
url: i.url,
|
|
38374
|
+
labels: i.labels.map((l) => l.name)
|
|
38375
|
+
}));
|
|
38376
|
+
} catch {
|
|
38377
|
+
return null;
|
|
38378
|
+
}
|
|
38379
|
+
}
|
|
38380
|
+
|
|
38381
|
+
// src/commands/workflow.ts
|
|
38382
|
+
var execAsync7 = promisify7(exec7);
|
|
37853
38383
|
function workflowCommand(program2) {
|
|
37854
38384
|
const workflow = program2.command("workflow").description("Run predefined workflow shortcuts");
|
|
37855
|
-
workflow.command("pr-ready").description("Prepare branch for pull request").action(async () => {
|
|
38385
|
+
workflow.command("pr-ready").description("Prepare branch for pull request (fetch, rebase, push)").action(async () => {
|
|
37856
38386
|
try {
|
|
37857
|
-
|
|
38387
|
+
const config = await loadConfig();
|
|
38388
|
+
const mainBranch = config?.project?.mainBranch ?? "main";
|
|
38389
|
+
const repoState = await getRepoState();
|
|
38390
|
+
if (isProtectedBranch(repoState.currentBranch, config)) {
|
|
38391
|
+
console.error(source_default.red(` ✖ You're on ${source_default.bold(repoState.currentBranch)}.`) + source_default.dim(" Switch to a feature branch first."));
|
|
38392
|
+
process.exit(1);
|
|
38393
|
+
}
|
|
38394
|
+
console.log(`
|
|
38395
|
+
Preparing ${source_default.cyan(repoState.currentBranch)} for PR against ` + `${source_default.dim(mainBranch)}...
|
|
37858
38396
|
`);
|
|
38397
|
+
let stashed = false;
|
|
38398
|
+
if (!repoState.isClean) {
|
|
38399
|
+
console.log(source_default.yellow(" ⚠ Uncommitted changes detected."));
|
|
38400
|
+
const { action } = await esm_default12.prompt([{
|
|
38401
|
+
type: "list",
|
|
38402
|
+
name: "action",
|
|
38403
|
+
message: "What would you like to do?",
|
|
38404
|
+
choices: [
|
|
38405
|
+
{ name: "Stash changes, rebase, then restore them", value: "stash" },
|
|
38406
|
+
{ name: "Abort", value: "abort" }
|
|
38407
|
+
]
|
|
38408
|
+
}]);
|
|
38409
|
+
if (action === "abort") {
|
|
38410
|
+
console.log(" Aborted.");
|
|
38411
|
+
return;
|
|
38412
|
+
}
|
|
38413
|
+
displayStep('git stash push -m "hermes pr-ready auto-stash"');
|
|
38414
|
+
await executeGitCommand('git stash push -m "hermes pr-ready auto-stash"');
|
|
38415
|
+
stashed = true;
|
|
38416
|
+
console.log();
|
|
38417
|
+
}
|
|
37859
38418
|
const steps = [
|
|
37860
38419
|
{ cmd: "git fetch origin", desc: "Fetching latest changes" },
|
|
37861
|
-
{ cmd:
|
|
38420
|
+
{ cmd: `git rebase origin/${mainBranch}`, desc: `Rebasing on ${mainBranch}` },
|
|
37862
38421
|
{ cmd: "git push --force-with-lease", desc: "Pushing changes safely" }
|
|
37863
38422
|
];
|
|
37864
38423
|
for (const { cmd, desc } of steps) {
|
|
37865
|
-
console.log(`
|
|
37866
|
-
${desc}...`);
|
|
38424
|
+
console.log(` ${desc}...`);
|
|
37867
38425
|
displayStep(cmd);
|
|
37868
38426
|
await executeGitCommand(cmd);
|
|
37869
38427
|
}
|
|
38428
|
+
if (stashed) {
|
|
38429
|
+
console.log(`
|
|
38430
|
+
Restoring stashed changes...`);
|
|
38431
|
+
displayStep("git stash pop");
|
|
38432
|
+
await executeGitCommand("git stash pop");
|
|
38433
|
+
}
|
|
37870
38434
|
displaySuccess("Branch ready for PR!");
|
|
37871
|
-
console.log(
|
|
38435
|
+
console.log(source_default.dim(`
|
|
38436
|
+
Next: gh pr create or open your hosting UI`));
|
|
38437
|
+
console.log();
|
|
37872
38438
|
} catch (error3) {
|
|
37873
|
-
console.error(
|
|
38439
|
+
console.error(source_default.red(`
|
|
38440
|
+
✖ ` + (error3 instanceof Error ? error3.message : error3)));
|
|
37874
38441
|
process.exit(1);
|
|
37875
38442
|
}
|
|
37876
38443
|
});
|
|
37877
|
-
workflow.command("daily-sync").description("Daily
|
|
38444
|
+
workflow.command("daily-sync").description("Daily standup: what you did, what's next, and any blockers").action(async () => {
|
|
37878
38445
|
try {
|
|
37879
|
-
console.log(`\uD83C\uDF05 Running daily sync...
|
|
37880
|
-
`);
|
|
37881
|
-
displayStep("git fetch --all --prune");
|
|
37882
|
-
await executeGitCommand("git fetch --all --prune");
|
|
37883
|
-
const repoState = await getRepoState();
|
|
37884
38446
|
console.log(`
|
|
37885
|
-
|
|
37886
|
-
|
|
37887
|
-
|
|
37888
|
-
|
|
37889
|
-
|
|
37890
|
-
|
|
37891
|
-
|
|
37892
|
-
|
|
38447
|
+
` + source_default.bold("Daily sync") + source_default.dim(` — gathering data...
|
|
38448
|
+
`));
|
|
38449
|
+
displayStep("git fetch --all --prune");
|
|
38450
|
+
await executeGitCommand("git fetch --all --prune").catch(() => {});
|
|
38451
|
+
const todayDate = todayKey();
|
|
38452
|
+
const yesterdayDate = lastWorkdayKey();
|
|
38453
|
+
const [repoState, stashList, mergedBranches, yesterdayCommits, todayCommits, yesterdayPlan] = await Promise.all([
|
|
38454
|
+
getRepoState(),
|
|
38455
|
+
execAsync7("git stash list").then((r) => r.stdout.trim().split(`
|
|
38456
|
+
`).filter(Boolean)).catch(() => []),
|
|
38457
|
+
execAsync7("git branch --merged HEAD").then((r) => r.stdout.trim().split(`
|
|
38458
|
+
`).map((b) => b.trim().replace(/^\* /, "")).filter((b) => b && !["main", "master", "staging", "production", "develop"].includes(b))).catch(() => []),
|
|
38459
|
+
getCommitsSince(yesterdayDate),
|
|
38460
|
+
getCommitsSince(todayDate),
|
|
38461
|
+
readPlan(yesterdayDate)
|
|
38462
|
+
]);
|
|
38463
|
+
const blockers = await detectBlockers(repoState);
|
|
38464
|
+
let todayPlan = await readPlan(todayDate);
|
|
38465
|
+
if (!todayPlan) {
|
|
38466
|
+
console.log(source_default.yellow(` No plan set for today (${todayDate}).
|
|
38467
|
+
`));
|
|
38468
|
+
const [claudeTodos, githubIssues] = await Promise.all([
|
|
38469
|
+
getClaudeCodeTodos().catch(() => []),
|
|
38470
|
+
getAssignedIssues().catch(() => null)
|
|
38471
|
+
]);
|
|
38472
|
+
const choices = [
|
|
38473
|
+
{ name: "Type it myself", value: "manual" },
|
|
38474
|
+
{ name: "Let AI suggest based on my repo", value: "ai" }
|
|
38475
|
+
];
|
|
38476
|
+
if (claudeTodos.length > 0) {
|
|
38477
|
+
choices.push({
|
|
38478
|
+
name: `Import from Claude Code ${source_default.dim(`(${claudeTodos.length} pending task${claudeTodos.length !== 1 ? "s" : ""})`)}`,
|
|
38479
|
+
value: "claude"
|
|
38480
|
+
});
|
|
38481
|
+
}
|
|
38482
|
+
if (githubIssues !== null) {
|
|
38483
|
+
choices.push({
|
|
38484
|
+
name: `Import from GitHub issues ${source_default.dim(`(${githubIssues.length} open)`)}`,
|
|
38485
|
+
value: "github"
|
|
38486
|
+
});
|
|
38487
|
+
}
|
|
38488
|
+
choices.push({ name: "Skip for now", value: "skip" });
|
|
38489
|
+
const { planMode } = await esm_default12.prompt([{
|
|
38490
|
+
type: "list",
|
|
38491
|
+
name: "planMode",
|
|
38492
|
+
message: "What are you focusing on today?",
|
|
38493
|
+
choices
|
|
38494
|
+
}]);
|
|
38495
|
+
todayPlan = await resolvePlan(planMode, {
|
|
38496
|
+
repoState,
|
|
38497
|
+
yesterdayPlan,
|
|
38498
|
+
yesterdayCommits,
|
|
38499
|
+
todayCommits,
|
|
38500
|
+
blockers,
|
|
38501
|
+
claudeTodos,
|
|
38502
|
+
githubIssues: githubIssues ?? []
|
|
38503
|
+
});
|
|
38504
|
+
if (todayPlan) {
|
|
38505
|
+
await writePlan(todayDate, todayPlan);
|
|
38506
|
+
}
|
|
38507
|
+
console.log();
|
|
38508
|
+
}
|
|
38509
|
+
const yesterdaySection = yesterdayCommits.length > 0 ? yesterdayCommits.map((c) => `- ${c.subject}`).join(`
|
|
38510
|
+
`) : "(no commits found)";
|
|
38511
|
+
const todayCommitsSection = todayCommits.length > 0 ? todayCommits.map((c) => `- ${c.subject}`).join(`
|
|
38512
|
+
`) : "(no commits yet today)";
|
|
38513
|
+
const blockersSection = blockers.length > 0 ? blockers.map((b) => `- [${b.severity}] ${b.message}`).join(`
|
|
38514
|
+
`) : "(none)";
|
|
38515
|
+
const prompt2 = `You are a developer writing a daily standup update for their team.
|
|
38516
|
+
|
|
38517
|
+
Use the data below to write a clear, concise standup. Write it in first person.
|
|
38518
|
+
Keep each section to 1-3 sentences. Be specific — mention actual work items from the commit subjects.
|
|
38519
|
+
If there are no commits, infer from the plan what was likely being worked on.
|
|
38520
|
+
|
|
38521
|
+
---
|
|
38522
|
+
YESTERDAY'S INTENDED PLAN:
|
|
38523
|
+
${yesterdayPlan ?? "(not recorded)"}
|
|
38524
|
+
|
|
38525
|
+
COMMITS FROM YESTERDAY / LAST WORKDAY:
|
|
38526
|
+
${yesterdaySection}
|
|
38527
|
+
|
|
38528
|
+
COMMITS FROM TODAY SO FAR:
|
|
38529
|
+
${todayCommitsSection}
|
|
38530
|
+
|
|
38531
|
+
TODAY'S PLAN:
|
|
38532
|
+
${todayPlan}
|
|
38533
|
+
|
|
38534
|
+
BLOCKERS / REPO STATE:
|
|
38535
|
+
${blockersSection}
|
|
38536
|
+
|
|
38537
|
+
CURRENT BRANCH: ${repoState.currentBranch}
|
|
38538
|
+
---
|
|
38539
|
+
|
|
38540
|
+
Format your response as exactly three labelled sections:
|
|
38541
|
+
|
|
38542
|
+
Yesterday: <what was accomplished>
|
|
38543
|
+
Today: <what will be worked on>
|
|
38544
|
+
Blockers: <blockers or "None">
|
|
38545
|
+
|
|
38546
|
+
No bullet points, no markdown, no extra commentary.`;
|
|
38547
|
+
console.log(` Generating standup...
|
|
38548
|
+
`);
|
|
38549
|
+
const standup = await getAISuggestion(prompt2);
|
|
38550
|
+
const divider = source_default.dim(" " + "─".repeat(52));
|
|
38551
|
+
console.log(divider);
|
|
38552
|
+
standup.split(`
|
|
38553
|
+
`).forEach((line) => {
|
|
38554
|
+
const trimmed = line.trim();
|
|
38555
|
+
if (!trimmed)
|
|
38556
|
+
return;
|
|
38557
|
+
const colonIdx = trimmed.indexOf(":");
|
|
38558
|
+
if (colonIdx > 0 && colonIdx < 15) {
|
|
38559
|
+
const label = trimmed.slice(0, colonIdx);
|
|
38560
|
+
const body = trimmed.slice(colonIdx + 1).trim();
|
|
38561
|
+
console.log(` ${source_default.bold.cyan(label + ":")} ${body}`);
|
|
38562
|
+
} else {
|
|
38563
|
+
console.log(" " + trimmed);
|
|
38564
|
+
}
|
|
38565
|
+
});
|
|
38566
|
+
console.log(divider);
|
|
38567
|
+
const notes = [];
|
|
38568
|
+
if (repoState.behind > 0)
|
|
38569
|
+
notes.push(source_default.yellow(`${repoState.behind} commits behind`) + source_default.dim(" — hermes sync"));
|
|
38570
|
+
if (repoState.ahead > 0)
|
|
38571
|
+
notes.push(source_default.cyan(`${repoState.ahead} commits ahead`) + source_default.dim(" — git push"));
|
|
38572
|
+
if (!repoState.isClean)
|
|
38573
|
+
notes.push(source_default.yellow("uncommitted changes"));
|
|
38574
|
+
if (mergedBranches.length > 0)
|
|
38575
|
+
notes.push(source_default.dim(`${mergedBranches.length} merged branch${mergedBranches.length !== 1 ? "es" : ""} to clean up — hermes workflow cleanup`));
|
|
38576
|
+
if (stashList.length > 0)
|
|
38577
|
+
notes.push(source_default.dim(`${stashList.length} stash${stashList.length !== 1 ? "es" : ""} saved`));
|
|
38578
|
+
if (notes.length > 0) {
|
|
38579
|
+
console.log(`
|
|
38580
|
+
${source_default.bold("Repo notes")}`);
|
|
38581
|
+
notes.forEach((n) => console.log(` • ${n}`));
|
|
37893
38582
|
}
|
|
37894
|
-
|
|
38583
|
+
console.log();
|
|
37895
38584
|
} catch (error3) {
|
|
37896
|
-
console.error(
|
|
38585
|
+
console.error(source_default.red(`
|
|
38586
|
+
✖ ` + (error3 instanceof Error ? error3.message : error3)));
|
|
37897
38587
|
process.exit(1);
|
|
37898
38588
|
}
|
|
37899
38589
|
});
|
|
37900
|
-
workflow.command("quick-commit").description("Stage changed files and commit with an AI-generated message").option("-a, --all", "Stage all changes (default: only already-staged files)").action(async (options) => {
|
|
38590
|
+
workflow.command("quick-commit").description("Stage changed files and commit with an AI-generated message").option("-a, --all", "Stage all changes (default: only already-staged files)").option("-p, --push", "Push after committing").action(async (options) => {
|
|
37901
38591
|
try {
|
|
37902
|
-
console.log(
|
|
38592
|
+
console.log(`
|
|
38593
|
+
Quick commit...
|
|
37903
38594
|
`);
|
|
37904
38595
|
const repoState = await getRepoState();
|
|
37905
38596
|
if (repoState.isClean) {
|
|
37906
|
-
console.log("
|
|
38597
|
+
console.log(" " + source_default.green("✓") + " Nothing to commit");
|
|
37907
38598
|
return;
|
|
37908
38599
|
}
|
|
37909
38600
|
if (options.all) {
|
|
@@ -37913,14 +38604,14 @@ ${desc}...`);
|
|
|
37913
38604
|
const diff = await executeGitCommand("git diff --cached --stat");
|
|
37914
38605
|
const diffFull = await executeGitCommand("git diff --cached");
|
|
37915
38606
|
if (!diff.trim()) {
|
|
37916
|
-
console.log("
|
|
38607
|
+
console.log(" " + source_default.yellow("⚠") + " No staged changes. Use --all to stage everything, or stage files first.");
|
|
37917
38608
|
return;
|
|
37918
38609
|
}
|
|
38610
|
+
console.log(" Staged changes:");
|
|
38611
|
+
diff.trim().split(`
|
|
38612
|
+
`).forEach((l) => console.log(" " + source_default.dim(l)));
|
|
37919
38613
|
console.log(`
|
|
37920
|
-
|
|
37921
|
-
console.log(diff);
|
|
37922
|
-
console.log(`
|
|
37923
|
-
\uD83E\uDD16 Generating commit message...`);
|
|
38614
|
+
Generating commit message...`);
|
|
37924
38615
|
const message = await getAISuggestion(`Generate a concise git commit message for these changes.
|
|
37925
38616
|
|
|
37926
38617
|
Rules:
|
|
@@ -37933,68 +38624,219 @@ Rules:
|
|
|
37933
38624
|
Diff:
|
|
37934
38625
|
${diffFull.slice(0, 8000)}`);
|
|
37935
38626
|
console.log(`
|
|
37936
|
-
|
|
37937
|
-
${source_default.cyan(message)}
|
|
38627
|
+
Proposed: ${source_default.cyan(message)}
|
|
37938
38628
|
`);
|
|
37939
|
-
const { action } = await esm_default12.prompt([
|
|
37940
|
-
|
|
37941
|
-
|
|
37942
|
-
|
|
37943
|
-
|
|
37944
|
-
|
|
37945
|
-
|
|
37946
|
-
|
|
37947
|
-
|
|
37948
|
-
|
|
37949
|
-
}
|
|
37950
|
-
]);
|
|
38629
|
+
const { action } = await esm_default12.prompt([{
|
|
38630
|
+
type: "list",
|
|
38631
|
+
name: "action",
|
|
38632
|
+
message: "Commit with this message?",
|
|
38633
|
+
choices: [
|
|
38634
|
+
{ name: "Yes, commit", value: "commit" },
|
|
38635
|
+
{ name: "Edit message", value: "edit" },
|
|
38636
|
+
{ name: "Cancel", value: "cancel" }
|
|
38637
|
+
]
|
|
38638
|
+
}]);
|
|
37951
38639
|
if (action === "cancel") {
|
|
37952
|
-
console.log("Cancelled. Changes remain staged.");
|
|
38640
|
+
console.log(" Cancelled. Changes remain staged.");
|
|
37953
38641
|
return;
|
|
37954
38642
|
}
|
|
37955
38643
|
let finalMessage = message;
|
|
37956
38644
|
if (action === "edit") {
|
|
37957
|
-
const { edited } = await esm_default12.prompt([
|
|
37958
|
-
|
|
37959
|
-
|
|
37960
|
-
|
|
37961
|
-
|
|
37962
|
-
|
|
37963
|
-
}
|
|
37964
|
-
]);
|
|
38645
|
+
const { edited } = await esm_default12.prompt([{
|
|
38646
|
+
type: "input",
|
|
38647
|
+
name: "edited",
|
|
38648
|
+
message: "Commit message:",
|
|
38649
|
+
default: message
|
|
38650
|
+
}]);
|
|
37965
38651
|
finalMessage = edited;
|
|
37966
38652
|
}
|
|
37967
38653
|
displayStep(`git commit -m "${finalMessage}"`);
|
|
37968
38654
|
await executeGitCommand(`git commit -m ${JSON.stringify(finalMessage)}`);
|
|
37969
38655
|
displaySuccess("Committed!");
|
|
38656
|
+
if (options.push) {
|
|
38657
|
+
console.log();
|
|
38658
|
+
const freshState = await getRepoState();
|
|
38659
|
+
const pushCmd = freshState.remoteTracking ? "git push" : `git push -u origin ${freshState.currentBranch}`;
|
|
38660
|
+
displayStep(pushCmd);
|
|
38661
|
+
await executeGitCommand(pushCmd);
|
|
38662
|
+
displaySuccess("Pushed!");
|
|
38663
|
+
}
|
|
38664
|
+
console.log();
|
|
37970
38665
|
} catch (error3) {
|
|
37971
|
-
console.error(
|
|
38666
|
+
console.error(source_default.red(`
|
|
38667
|
+
✖ ` + (error3 instanceof Error ? error3.message : error3)));
|
|
38668
|
+
process.exit(1);
|
|
38669
|
+
}
|
|
38670
|
+
});
|
|
38671
|
+
workflow.command("cleanup").description("Delete branches that have already been merged").option("--remote", "Also delete the corresponding remote tracking branches").action(async (options) => {
|
|
38672
|
+
try {
|
|
38673
|
+
const config = await loadConfig();
|
|
38674
|
+
const mainBranch = config?.project?.mainBranch ?? "main";
|
|
38675
|
+
const protected_ = config?.project?.protectedBranches ?? ["main", "master", "staging", "production"];
|
|
38676
|
+
console.log(`
|
|
38677
|
+
Scanning for merged branches...
|
|
38678
|
+
`);
|
|
38679
|
+
displayStep(`git fetch origin --prune`);
|
|
38680
|
+
await executeGitCommand("git fetch origin --prune").catch(() => {});
|
|
38681
|
+
const { stdout: branchOutput } = await execAsync7(`git branch --merged ${mainBranch}`);
|
|
38682
|
+
const repoState = await getRepoState();
|
|
38683
|
+
const merged = branchOutput.split(`
|
|
38684
|
+
`).map((b) => b.trim().replace(/^\* /, "")).filter((b) => b && !protected_.includes(b) && b !== repoState.currentBranch);
|
|
38685
|
+
if (merged.length === 0) {
|
|
38686
|
+
console.log(" " + source_default.green("✓") + " No merged branches to clean up.");
|
|
38687
|
+
console.log();
|
|
38688
|
+
return;
|
|
38689
|
+
}
|
|
38690
|
+
console.log(` Found ${source_default.yellow(merged.length)} merged branch${merged.length !== 1 ? "es" : ""}:
|
|
38691
|
+
`);
|
|
38692
|
+
merged.forEach((b) => console.log(` ${source_default.dim("•")} ${b}`));
|
|
38693
|
+
console.log();
|
|
38694
|
+
const { selected } = await esm_default12.prompt([{
|
|
38695
|
+
type: "checkbox",
|
|
38696
|
+
name: "selected",
|
|
38697
|
+
message: "Select branches to delete:",
|
|
38698
|
+
choices: merged.map((b) => ({ name: b, value: b, checked: true }))
|
|
38699
|
+
}]);
|
|
38700
|
+
if (selected.length === 0) {
|
|
38701
|
+
console.log(" Nothing selected. Exiting.");
|
|
38702
|
+
return;
|
|
38703
|
+
}
|
|
38704
|
+
for (const branch of selected) {
|
|
38705
|
+
displayStep(`git branch -d ${branch}`);
|
|
38706
|
+
await executeGitCommand(`git branch -d ${branch}`);
|
|
38707
|
+
if (options.remote) {
|
|
38708
|
+
try {
|
|
38709
|
+
displayStep(`git push origin --delete ${branch}`);
|
|
38710
|
+
await executeGitCommand(`git push origin --delete ${branch}`);
|
|
38711
|
+
} catch {
|
|
38712
|
+
console.log(source_default.dim(` (remote branch ${branch} not found — skipping)`));
|
|
38713
|
+
}
|
|
38714
|
+
}
|
|
38715
|
+
}
|
|
38716
|
+
displaySuccess(`Deleted ${selected.length} branch${selected.length !== 1 ? "es" : ""}!`);
|
|
38717
|
+
console.log();
|
|
38718
|
+
} catch (error3) {
|
|
38719
|
+
console.error(source_default.red(`
|
|
38720
|
+
✖ ` + (error3 instanceof Error ? error3.message : error3)));
|
|
37972
38721
|
process.exit(1);
|
|
37973
38722
|
}
|
|
37974
38723
|
});
|
|
37975
38724
|
workflow.command("list").description("List available workflow shortcuts").action(async () => {
|
|
37976
38725
|
const config = await loadConfig();
|
|
37977
|
-
console.log(
|
|
38726
|
+
console.log(`
|
|
38727
|
+
` + source_default.bold("Built-in workflows") + `
|
|
37978
38728
|
`);
|
|
37979
|
-
console.log("
|
|
37980
|
-
console.log("
|
|
37981
|
-
console.log("
|
|
37982
|
-
console.log("
|
|
37983
|
-
if (config?.workflows) {
|
|
38729
|
+
console.log(` ${source_default.cyan("pr-ready")} Rebase on main and push — safe PR prep with stash guard`);
|
|
38730
|
+
console.log(` ${source_default.cyan("daily-sync")} Fetch, show status, surface what to act on`);
|
|
38731
|
+
console.log(` ${source_default.cyan("quick-commit")} AI commit message; add ${source_default.dim("--push")} to commit + push`);
|
|
38732
|
+
console.log(` ${source_default.cyan("cleanup")} Delete merged branches; add ${source_default.dim("--remote")} to also clean remote`);
|
|
38733
|
+
if (config?.workflows && Object.keys(config.workflows).length > 0) {
|
|
37984
38734
|
console.log(`
|
|
37985
|
-
Project
|
|
37986
|
-
|
|
37987
|
-
|
|
37988
|
-
console.log(`
|
|
37989
|
-
}
|
|
38735
|
+
` + source_default.bold("Project workflows") + source_default.dim(" (.hermes/config.json)") + `
|
|
38736
|
+
`);
|
|
38737
|
+
for (const [name, steps] of Object.entries(config.workflows)) {
|
|
38738
|
+
console.log(` ${source_default.cyan(name.padEnd(14))} ${source_default.dim(steps.join(" → "))}`);
|
|
38739
|
+
}
|
|
37990
38740
|
} else {
|
|
37991
|
-
console.log(
|
|
38741
|
+
console.log(`
|
|
38742
|
+
` + source_default.dim("No project workflows defined. Run hermes init to set them up."));
|
|
37992
38743
|
}
|
|
38744
|
+
console.log();
|
|
37993
38745
|
});
|
|
37994
38746
|
}
|
|
38747
|
+
async function resolvePlan(mode, ctx) {
|
|
38748
|
+
if (mode === "skip")
|
|
38749
|
+
return null;
|
|
38750
|
+
if (mode === "manual") {
|
|
38751
|
+
const { typed } = await esm_default12.prompt([{
|
|
38752
|
+
type: "input",
|
|
38753
|
+
name: "typed",
|
|
38754
|
+
message: "Today's plan:",
|
|
38755
|
+
validate: (v) => v.trim().length > 0 || "Please enter something"
|
|
38756
|
+
}]);
|
|
38757
|
+
return typed.trim();
|
|
38758
|
+
}
|
|
38759
|
+
if (mode === "ai") {
|
|
38760
|
+
process.stdout.write(source_default.dim(`
|
|
38761
|
+
Thinking...\r`));
|
|
38762
|
+
const suggested = await suggestTodayPlan({
|
|
38763
|
+
currentBranch: ctx.repoState.currentBranch,
|
|
38764
|
+
yesterdayPlan: ctx.yesterdayPlan,
|
|
38765
|
+
yesterdayCommits: ctx.yesterdayCommits,
|
|
38766
|
+
todayCommits: ctx.todayCommits,
|
|
38767
|
+
hasUncommittedChanges: ctx.repoState.hasUncommittedChanges,
|
|
38768
|
+
blockers: ctx.blockers
|
|
38769
|
+
});
|
|
38770
|
+
process.stdout.write(" ".repeat(30) + "\r");
|
|
38771
|
+
return confirmOrEdit(suggested);
|
|
38772
|
+
}
|
|
38773
|
+
if (mode === "claude") {
|
|
38774
|
+
const { selected } = await esm_default12.prompt([{
|
|
38775
|
+
type: "checkbox",
|
|
38776
|
+
name: "selected",
|
|
38777
|
+
message: "Select tasks to include in today's plan:",
|
|
38778
|
+
choices: ctx.claudeTodos.map((t) => ({
|
|
38779
|
+
name: `${t.status === "in_progress" ? source_default.yellow("(in progress) ") : ""}${t.content}` + source_default.dim(` [${t.priority}]`),
|
|
38780
|
+
value: t.content,
|
|
38781
|
+
checked: t.status === "in_progress"
|
|
38782
|
+
}))
|
|
38783
|
+
}]);
|
|
38784
|
+
if (selected.length === 0)
|
|
38785
|
+
return null;
|
|
38786
|
+
const plan = selected.join("; ");
|
|
38787
|
+
return confirmOrEdit(plan);
|
|
38788
|
+
}
|
|
38789
|
+
if (mode === "github") {
|
|
38790
|
+
if (ctx.githubIssues.length === 0) {
|
|
38791
|
+
console.log(source_default.dim(" No open issues assigned to you."));
|
|
38792
|
+
return null;
|
|
38793
|
+
}
|
|
38794
|
+
const { selected } = await esm_default12.prompt([{
|
|
38795
|
+
type: "checkbox",
|
|
38796
|
+
name: "selected",
|
|
38797
|
+
message: "Select issues to include in today's plan:",
|
|
38798
|
+
choices: ctx.githubIssues.map((i) => ({
|
|
38799
|
+
name: `#${i.number} ${i.title}` + (i.labels.length > 0 ? source_default.dim(` [${i.labels.join(", ")}]`) : ""),
|
|
38800
|
+
value: `#${i.number} ${i.title}`,
|
|
38801
|
+
checked: true
|
|
38802
|
+
}))
|
|
38803
|
+
}]);
|
|
38804
|
+
if (selected.length === 0)
|
|
38805
|
+
return null;
|
|
38806
|
+
const plan = selected.join("; ");
|
|
38807
|
+
return confirmOrEdit(plan);
|
|
38808
|
+
}
|
|
38809
|
+
return null;
|
|
38810
|
+
}
|
|
38811
|
+
async function confirmOrEdit(suggestion) {
|
|
38812
|
+
console.log(`
|
|
38813
|
+
${source_default.bold("Plan:")} ${source_default.cyan(suggestion)}
|
|
38814
|
+
`);
|
|
38815
|
+
const { action } = await esm_default12.prompt([{
|
|
38816
|
+
type: "list",
|
|
38817
|
+
name: "action",
|
|
38818
|
+
message: "Use this?",
|
|
38819
|
+
choices: [
|
|
38820
|
+
{ name: "Yes", value: "accept" },
|
|
38821
|
+
{ name: "Edit", value: "edit" },
|
|
38822
|
+
{ name: "Skip", value: "skip" }
|
|
38823
|
+
]
|
|
38824
|
+
}]);
|
|
38825
|
+
if (action === "accept")
|
|
38826
|
+
return suggestion;
|
|
38827
|
+
if (action === "skip")
|
|
38828
|
+
return null;
|
|
38829
|
+
const { edited } = await esm_default12.prompt([{
|
|
38830
|
+
type: "input",
|
|
38831
|
+
name: "edited",
|
|
38832
|
+
message: "Today's plan:",
|
|
38833
|
+
default: suggestion
|
|
38834
|
+
}]);
|
|
38835
|
+
return edited.trim() || null;
|
|
38836
|
+
}
|
|
37995
38837
|
|
|
37996
38838
|
// src/commands/config.ts
|
|
37997
|
-
var VALID_PROVIDERS = ["anthropic", "openai", "gemini"];
|
|
38839
|
+
var VALID_PROVIDERS = ["anthropic", "openai", "gemini", "claude-code"];
|
|
37998
38840
|
var KEY_ALIASES = {
|
|
37999
38841
|
provider: "provider",
|
|
38000
38842
|
"anthropic-key": "anthropicApiKey",
|
|
@@ -38002,7 +38844,7 @@ var KEY_ALIASES = {
|
|
|
38002
38844
|
"gemini-key": "geminiApiKey"
|
|
38003
38845
|
};
|
|
38004
38846
|
var KEY_DESCRIPTIONS = {
|
|
38005
|
-
provider: "Default AI provider (anthropic | openai | gemini)",
|
|
38847
|
+
provider: "Default AI provider (anthropic | openai | gemini | claude-code)",
|
|
38006
38848
|
anthropicApiKey: "Anthropic API key",
|
|
38007
38849
|
openaiApiKey: "OpenAI API key",
|
|
38008
38850
|
geminiApiKey: "Gemini API key"
|
|
@@ -38100,55 +38942,71 @@ function configCommand(program2) {
|
|
|
38100
38942
|
console.log(source_default.dim(` ${GLOBAL_CONFIG_PATH}
|
|
38101
38943
|
`));
|
|
38102
38944
|
const globalConfig = await loadGlobalConfig();
|
|
38103
|
-
const
|
|
38104
|
-
|
|
38105
|
-
|
|
38106
|
-
name: "provider",
|
|
38107
|
-
message: "Which AI provider do you want to use?",
|
|
38108
|
-
choices: [
|
|
38109
|
-
{ name: "Anthropic (Claude)", value: "anthropic" },
|
|
38110
|
-
{ name: "OpenAI (GPT-4o)", value: "openai" },
|
|
38111
|
-
{ name: "Google (Gemini)", value: "gemini" },
|
|
38112
|
-
{ name: "Auto-detect (use whichever key is available)", value: "" }
|
|
38113
|
-
],
|
|
38114
|
-
default: globalConfig.provider || ""
|
|
38115
|
-
}
|
|
38945
|
+
const [claudeAvailable, codexAvailable] = await Promise.all([
|
|
38946
|
+
isClaudeCodeAvailable(),
|
|
38947
|
+
isCodexAvailable()
|
|
38116
38948
|
]);
|
|
38949
|
+
const cliChoices = [
|
|
38950
|
+
claudeAvailable ? { name: `Claude Code CLI ${source_default.green("(detected)")}`, value: "claude-code" } : { name: `Claude Code CLI ${source_default.dim("(not found)")}`, value: "claude-code", disabled: true },
|
|
38951
|
+
codexAvailable ? { name: `OpenAI Codex CLI ${source_default.green("(detected)")}`, value: "codex" } : { name: `OpenAI Codex CLI ${source_default.dim("(not found)")}`, value: "codex", disabled: true }
|
|
38952
|
+
];
|
|
38953
|
+
const providerChoices = [
|
|
38954
|
+
...cliChoices,
|
|
38955
|
+
{ name: "Anthropic API (ANTHROPIC_API_KEY)", value: "anthropic" },
|
|
38956
|
+
{ name: "OpenAI API (OPENAI_API_KEY)", value: "openai" },
|
|
38957
|
+
{ name: "Google Gemini (GEMINI_API_KEY)", value: "gemini" },
|
|
38958
|
+
{ name: "Auto-detect (use whichever is available)", value: "" }
|
|
38959
|
+
];
|
|
38960
|
+
const defaultProvider = globalConfig.provider || (claudeAvailable ? "claude-code" : codexAvailable ? "codex" : "");
|
|
38961
|
+
const { provider } = await esm_default12.prompt([{
|
|
38962
|
+
type: "list",
|
|
38963
|
+
name: "provider",
|
|
38964
|
+
message: "Which AI provider do you want to use?",
|
|
38965
|
+
choices: providerChoices,
|
|
38966
|
+
default: defaultProvider
|
|
38967
|
+
}]);
|
|
38117
38968
|
if (provider) {
|
|
38118
38969
|
globalConfig.provider = provider;
|
|
38119
38970
|
} else {
|
|
38120
38971
|
delete globalConfig.provider;
|
|
38121
38972
|
}
|
|
38122
|
-
|
|
38973
|
+
if (provider === "claude-code" || provider === "codex") {
|
|
38974
|
+
await saveGlobalConfig(globalConfig);
|
|
38975
|
+
const label = provider === "claude-code" ? "claude CLI" : "codex CLI";
|
|
38976
|
+
console.log(`
|
|
38977
|
+
${source_default.green("✓")} Provider set to ${source_default.cyan(provider)}`);
|
|
38978
|
+
console.log(source_default.dim(` Hermes will use the ${label} — no API key required.
|
|
38979
|
+
`));
|
|
38980
|
+
return;
|
|
38981
|
+
}
|
|
38982
|
+
const apiProviders = provider ? [provider] : ["anthropic", "openai", "gemini"];
|
|
38123
38983
|
const keyPrompts = {
|
|
38124
38984
|
anthropic: { field: "anthropicApiKey", envVar: "ANTHROPIC_API_KEY", hint: "sk-ant-..." },
|
|
38125
38985
|
openai: { field: "openaiApiKey", envVar: "OPENAI_API_KEY", hint: "sk-..." },
|
|
38126
38986
|
gemini: { field: "geminiApiKey", envVar: "GEMINI_API_KEY", hint: "AIza..." }
|
|
38127
38987
|
};
|
|
38128
|
-
for (const p of
|
|
38988
|
+
for (const p of apiProviders) {
|
|
38129
38989
|
const { field, envVar, hint } = keyPrompts[p];
|
|
38130
38990
|
const existing = globalConfig[field];
|
|
38131
38991
|
const fromEnv = process.env[envVar];
|
|
38132
38992
|
if (fromEnv) {
|
|
38133
|
-
console.log(source_default.dim(` ${envVar} already set in environment
|
|
38993
|
+
console.log(source_default.dim(` ${envVar} already set in environment — skipping.`));
|
|
38134
38994
|
continue;
|
|
38135
38995
|
}
|
|
38136
|
-
const { key: key2 } = await esm_default12.prompt([
|
|
38137
|
-
|
|
38138
|
-
|
|
38139
|
-
|
|
38140
|
-
|
|
38141
|
-
|
|
38142
|
-
|
|
38143
|
-
}
|
|
38144
|
-
]);
|
|
38996
|
+
const { key: key2 } = await esm_default12.prompt([{
|
|
38997
|
+
type: "password",
|
|
38998
|
+
name: "key",
|
|
38999
|
+
message: `${p.charAt(0).toUpperCase() + p.slice(1)} API key (${hint}):`,
|
|
39000
|
+
default: existing ? "(keep existing)" : "",
|
|
39001
|
+
mask: "*"
|
|
39002
|
+
}]);
|
|
38145
39003
|
if (key2 && key2 !== "(keep existing)") {
|
|
38146
39004
|
globalConfig[field] = key2;
|
|
38147
39005
|
}
|
|
38148
39006
|
}
|
|
38149
39007
|
await saveGlobalConfig(globalConfig);
|
|
38150
39008
|
console.log(`
|
|
38151
|
-
${source_default.green("✓")} Configuration saved to ${source_default.dim(GLOBAL_CONFIG_PATH)}`);
|
|
39009
|
+
${source_default.green("✓")} Configuration saved to ${source_default.dim(GLOBAL_CONFIG_PATH)}`);
|
|
38152
39010
|
console.log(source_default.dim(` File permissions set to 600 (owner read/write only)
|
|
38153
39011
|
`));
|
|
38154
39012
|
console.log(` Run ${source_default.cyan("hermes config list")} to verify your setup.`);
|
|
@@ -38167,15 +39025,15 @@ Valid keys:`));
|
|
|
38167
39025
|
}
|
|
38168
39026
|
|
|
38169
39027
|
// src/commands/guard.ts
|
|
38170
|
-
import { writeFile as
|
|
38171
|
-
import { existsSync as
|
|
38172
|
-
import { exec as
|
|
38173
|
-
import { promisify as
|
|
39028
|
+
import { writeFile as writeFile6, readFile as readFile8, chmod as chmod2, mkdir as mkdir5 } from "fs/promises";
|
|
39029
|
+
import { existsSync as existsSync7 } from "fs";
|
|
39030
|
+
import { exec as exec9 } from "child_process";
|
|
39031
|
+
import { promisify as promisify9 } from "util";
|
|
38174
39032
|
|
|
38175
39033
|
// src/lib/secrets.ts
|
|
38176
|
-
import { exec as
|
|
38177
|
-
import { promisify as
|
|
38178
|
-
var
|
|
39034
|
+
import { exec as exec8 } from "child_process";
|
|
39035
|
+
import { promisify as promisify8 } from "util";
|
|
39036
|
+
var execAsync8 = promisify8(exec8);
|
|
38179
39037
|
var BLOCKED_FILENAMES = [
|
|
38180
39038
|
{ pattern: /^\.env$/, description: "Environment variable file" },
|
|
38181
39039
|
{ pattern: /^\.env\..+/, description: "Environment variable file" },
|
|
@@ -38304,13 +39162,13 @@ var SECRET_PATTERNS = [
|
|
|
38304
39162
|
}
|
|
38305
39163
|
];
|
|
38306
39164
|
async function getStagedFiles() {
|
|
38307
|
-
const { stdout } = await
|
|
39165
|
+
const { stdout } = await execAsync8("git diff --cached --name-only --diff-filter=ACMR");
|
|
38308
39166
|
return stdout.trim().split(`
|
|
38309
39167
|
`).filter(Boolean);
|
|
38310
39168
|
}
|
|
38311
39169
|
async function readStagedFile(file) {
|
|
38312
39170
|
try {
|
|
38313
|
-
const { stdout } = await
|
|
39171
|
+
const { stdout } = await execAsync8(`git show ":${file}"`, {
|
|
38314
39172
|
maxBuffer: 2 * 1024 * 1024
|
|
38315
39173
|
});
|
|
38316
39174
|
return stdout;
|
|
@@ -38408,7 +39266,7 @@ async function scanStagedFiles() {
|
|
|
38408
39266
|
}
|
|
38409
39267
|
|
|
38410
39268
|
// src/commands/guard.ts
|
|
38411
|
-
var
|
|
39269
|
+
var execAsync9 = promisify9(exec9);
|
|
38412
39270
|
function guardCommand(program2) {
|
|
38413
39271
|
const guard = program2.command("guard").description("Scan staged files for secrets and sensitive content before committing");
|
|
38414
39272
|
guard.option("--hook", "Run in non-interactive hook mode (exit 1 on blockers)").action(async (options) => {
|
|
@@ -38416,13 +39274,13 @@ function guardCommand(program2) {
|
|
|
38416
39274
|
});
|
|
38417
39275
|
guard.command("install-hook").description("Install hermes guard as a git pre-commit hook").action(async () => {
|
|
38418
39276
|
try {
|
|
38419
|
-
const gitDir = await
|
|
39277
|
+
const gitDir = await execAsync9("git rev-parse --git-dir").then((r) => r.stdout.trim()).catch(() => null);
|
|
38420
39278
|
if (!gitDir) {
|
|
38421
39279
|
console.error(source_default.red("❌ Not a git repository"));
|
|
38422
39280
|
process.exit(1);
|
|
38423
39281
|
}
|
|
38424
39282
|
const hooksDir = `${gitDir}/hooks`;
|
|
38425
|
-
await
|
|
39283
|
+
await mkdir5(hooksDir, { recursive: true });
|
|
38426
39284
|
const hookPath = `${hooksDir}/pre-commit`;
|
|
38427
39285
|
const hookScript = [
|
|
38428
39286
|
"#!/bin/sh",
|
|
@@ -38431,19 +39289,19 @@ function guardCommand(program2) {
|
|
|
38431
39289
|
].join(`
|
|
38432
39290
|
`) + `
|
|
38433
39291
|
`;
|
|
38434
|
-
if (
|
|
38435
|
-
const existing = await
|
|
39292
|
+
if (existsSync7(hookPath)) {
|
|
39293
|
+
const existing = await readFile8(hookPath, "utf-8");
|
|
38436
39294
|
if (existing.includes("hermes guard")) {
|
|
38437
39295
|
console.log(source_default.dim("Hook already installed."));
|
|
38438
39296
|
return;
|
|
38439
39297
|
}
|
|
38440
|
-
await
|
|
39298
|
+
await writeFile6(hookPath, existing.trimEnd() + `
|
|
38441
39299
|
|
|
38442
39300
|
` + hookScript.split(`
|
|
38443
39301
|
`).slice(1).join(`
|
|
38444
39302
|
`));
|
|
38445
39303
|
} else {
|
|
38446
|
-
await
|
|
39304
|
+
await writeFile6(hookPath, hookScript);
|
|
38447
39305
|
}
|
|
38448
39306
|
await chmod2(hookPath, 493);
|
|
38449
39307
|
console.log(`${source_default.green("✓")} Pre-commit hook installed at ${source_default.dim(hookPath)}`);
|
|
@@ -38455,17 +39313,17 @@ function guardCommand(program2) {
|
|
|
38455
39313
|
});
|
|
38456
39314
|
guard.command("uninstall-hook").description("Remove hermes guard from the git pre-commit hook").action(async () => {
|
|
38457
39315
|
try {
|
|
38458
|
-
const gitDir = await
|
|
39316
|
+
const gitDir = await execAsync9("git rev-parse --git-dir").then((r) => r.stdout.trim()).catch(() => null);
|
|
38459
39317
|
if (!gitDir) {
|
|
38460
39318
|
console.error(source_default.red("❌ Not a git repository"));
|
|
38461
39319
|
process.exit(1);
|
|
38462
39320
|
}
|
|
38463
39321
|
const hookPath = `${gitDir}/hooks/pre-commit`;
|
|
38464
|
-
if (!
|
|
39322
|
+
if (!existsSync7(hookPath)) {
|
|
38465
39323
|
console.log(source_default.dim("No pre-commit hook found."));
|
|
38466
39324
|
return;
|
|
38467
39325
|
}
|
|
38468
|
-
const content = await
|
|
39326
|
+
const content = await readFile8(hookPath, "utf-8");
|
|
38469
39327
|
if (!content.includes("hermes guard")) {
|
|
38470
39328
|
console.log(source_default.dim("hermes guard is not in the pre-commit hook."));
|
|
38471
39329
|
return;
|
|
@@ -38482,7 +39340,7 @@ function guardCommand(program2) {
|
|
|
38482
39340
|
await unlink(hookPath);
|
|
38483
39341
|
console.log(`${source_default.green("✓")} Pre-commit hook removed.`);
|
|
38484
39342
|
} else {
|
|
38485
|
-
await
|
|
39343
|
+
await writeFile6(hookPath, cleaned);
|
|
38486
39344
|
console.log(`${source_default.green("✓")} hermes guard removed from pre-commit hook.`);
|
|
38487
39345
|
}
|
|
38488
39346
|
} catch (error3) {
|
|
@@ -38555,7 +39413,7 @@ async function runScan(hookMode) {
|
|
|
38555
39413
|
}
|
|
38556
39414
|
if (action === "unstage") {
|
|
38557
39415
|
for (const f of result.blocked) {
|
|
38558
|
-
await
|
|
39416
|
+
await execAsync9(`git restore --staged "${f.file}"`);
|
|
38559
39417
|
console.log(source_default.dim(` Unstaged: ${f.file}`));
|
|
38560
39418
|
}
|
|
38561
39419
|
console.log(source_default.yellow(`
|
|
@@ -38672,9 +39530,14 @@ Be direct and specific. 3-4 sentences per finding max. No preamble.`);
|
|
|
38672
39530
|
}
|
|
38673
39531
|
|
|
38674
39532
|
// src/commands/update.ts
|
|
38675
|
-
import { exec as
|
|
38676
|
-
import { promisify as
|
|
38677
|
-
|
|
39533
|
+
import { exec as exec10 } from "child_process";
|
|
39534
|
+
import { promisify as promisify10 } from "util";
|
|
39535
|
+
import { writeFile as writeFile7, mkdir as mkdir6 } from "fs/promises";
|
|
39536
|
+
import { homedir as homedir3 } from "os";
|
|
39537
|
+
import path8 from "path";
|
|
39538
|
+
var CACHE_DIR = path8.join(homedir3(), ".hermes", "cache");
|
|
39539
|
+
var CACHE_FILE = path8.join(CACHE_DIR, "update-check.json");
|
|
39540
|
+
var execAsync10 = promisify10(exec10);
|
|
38678
39541
|
var PACKAGE_NAME = "hermes-git";
|
|
38679
39542
|
function updateCommand(program2, currentVersion) {
|
|
38680
39543
|
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) => {
|
|
@@ -38704,7 +39567,7 @@ function updateCommand(program2, currentVersion) {
|
|
|
38704
39567
|
const installCmd = buildInstallCommand(pm);
|
|
38705
39568
|
console.log(` Running: ${source_default.cyan(installCmd)}
|
|
38706
39569
|
`);
|
|
38707
|
-
const { stdout, stderr } = await
|
|
39570
|
+
const { stdout, stderr } = await execAsync10(installCmd, { timeout: 120000 });
|
|
38708
39571
|
const output = (stdout + stderr).trim();
|
|
38709
39572
|
if (output) {
|
|
38710
39573
|
output.split(`
|
|
@@ -38713,6 +39576,10 @@ function updateCommand(program2, currentVersion) {
|
|
|
38713
39576
|
});
|
|
38714
39577
|
console.log();
|
|
38715
39578
|
}
|
|
39579
|
+
try {
|
|
39580
|
+
await mkdir6(CACHE_DIR, { recursive: true });
|
|
39581
|
+
await writeFile7(CACHE_FILE, JSON.stringify({ lastChecked: Date.now(), latestVersion: latest }));
|
|
39582
|
+
} catch {}
|
|
38716
39583
|
console.log(source_default.green(" ✓ Updated to ") + source_default.bold(`v${latest}`) + source_default.dim(" Restart your terminal if the version does not change."));
|
|
38717
39584
|
} catch (error3) {
|
|
38718
39585
|
console.error(source_default.red(`
|
|
@@ -38751,20 +39618,20 @@ async function detectPackageManager() {
|
|
|
38751
39618
|
return "bun";
|
|
38752
39619
|
}
|
|
38753
39620
|
try {
|
|
38754
|
-
const { stdout } = await
|
|
39621
|
+
const { stdout } = await execAsync10("bun --version", { timeout: 3000 });
|
|
38755
39622
|
if (stdout.trim()) {
|
|
38756
39623
|
try {
|
|
38757
|
-
const { stdout: list } = await
|
|
39624
|
+
const { stdout: list } = await execAsync10("bun pm ls -g 2>/dev/null", { timeout: 3000 });
|
|
38758
39625
|
if (list.includes(PACKAGE_NAME))
|
|
38759
39626
|
return "bun";
|
|
38760
39627
|
} catch {}
|
|
38761
39628
|
}
|
|
38762
39629
|
} catch {}
|
|
38763
39630
|
try {
|
|
38764
|
-
const { stdout } = await
|
|
39631
|
+
const { stdout } = await execAsync10("pnpm --version", { timeout: 3000 });
|
|
38765
39632
|
if (stdout.trim()) {
|
|
38766
39633
|
try {
|
|
38767
|
-
const { stdout: list } = await
|
|
39634
|
+
const { stdout: list } = await execAsync10("pnpm list -g --depth=0 2>/dev/null", { timeout: 3000 });
|
|
38768
39635
|
if (list.includes(PACKAGE_NAME))
|
|
38769
39636
|
return "pnpm";
|
|
38770
39637
|
} catch {}
|
|
@@ -38784,13 +39651,13 @@ function buildInstallCommand(pm) {
|
|
|
38784
39651
|
}
|
|
38785
39652
|
|
|
38786
39653
|
// src/commands/commit.ts
|
|
38787
|
-
import { exec as
|
|
38788
|
-
import { promisify as
|
|
38789
|
-
var
|
|
38790
|
-
var execFileAsync =
|
|
39654
|
+
import { exec as exec11, execFile } from "child_process";
|
|
39655
|
+
import { promisify as promisify11 } from "util";
|
|
39656
|
+
var execAsync11 = promisify11(exec11);
|
|
39657
|
+
var execFileAsync = promisify11(execFile);
|
|
38791
39658
|
async function getStagedFiles2() {
|
|
38792
39659
|
try {
|
|
38793
|
-
const { stdout } = await
|
|
39660
|
+
const { stdout } = await execAsync11("git diff --cached --name-status");
|
|
38794
39661
|
return stdout.trim().split(`
|
|
38795
39662
|
`).filter(Boolean).map((line) => {
|
|
38796
39663
|
const [status, ...rest] = line.split("\t");
|
|
@@ -38802,12 +39669,12 @@ async function getStagedFiles2() {
|
|
|
38802
39669
|
}
|
|
38803
39670
|
async function getUnstagedFiles() {
|
|
38804
39671
|
try {
|
|
38805
|
-
const { stdout } = await
|
|
39672
|
+
const { stdout } = await execAsync11("git status --porcelain");
|
|
38806
39673
|
return stdout.trim().split(`
|
|
38807
39674
|
`).filter(Boolean).map((line) => {
|
|
38808
39675
|
const status = line.slice(0, 2).trim();
|
|
38809
|
-
const
|
|
38810
|
-
return { status, path:
|
|
39676
|
+
const path9 = line.slice(3).trim();
|
|
39677
|
+
return { status, path: path9 };
|
|
38811
39678
|
}).filter(({ status }) => status === "??" || status[0] === " " || status.length === 1);
|
|
38812
39679
|
} catch {
|
|
38813
39680
|
return [];
|
|
@@ -38815,7 +39682,7 @@ async function getUnstagedFiles() {
|
|
|
38815
39682
|
}
|
|
38816
39683
|
async function getRecentBranchCommits(n = 5) {
|
|
38817
39684
|
try {
|
|
38818
|
-
const { stdout } = await
|
|
39685
|
+
const { stdout } = await execAsync11(`git log --oneline -${n} --no-merges 2>/dev/null`);
|
|
38819
39686
|
return stdout.trim();
|
|
38820
39687
|
} catch {
|
|
38821
39688
|
return "";
|
|
@@ -38885,8 +39752,8 @@ function commitCommand(program2) {
|
|
|
38885
39752
|
try {
|
|
38886
39753
|
const repoState = await getRepoState();
|
|
38887
39754
|
if (options.all) {
|
|
38888
|
-
displayStep("git add -
|
|
38889
|
-
await
|
|
39755
|
+
displayStep("git add -A");
|
|
39756
|
+
await execAsync11("git add -A");
|
|
38890
39757
|
}
|
|
38891
39758
|
const staged = await getStagedFiles2();
|
|
38892
39759
|
if (staged.length === 0) {
|
|
@@ -38917,8 +39784,8 @@ function commitCommand(program2) {
|
|
|
38917
39784
|
console.log();
|
|
38918
39785
|
console.log(` Analyzing... ${source_default.dim(`[${name} / ${model}]`)}`);
|
|
38919
39786
|
const [stagedDiff, stagedStat] = await Promise.all([
|
|
38920
|
-
|
|
38921
|
-
|
|
39787
|
+
execAsync11("git diff --cached").then((r) => r.stdout),
|
|
39788
|
+
execAsync11("git diff --cached --stat").then((r) => r.stdout)
|
|
38922
39789
|
]);
|
|
38923
39790
|
const analysis = await analyzeChanges(repoState.currentBranch, stagedDiff, stagedStat, recentCommits);
|
|
38924
39791
|
console.log();
|
|
@@ -39045,14 +39912,14 @@ function commitCommand(program2) {
|
|
|
39045
39912
|
for (const f of files) {
|
|
39046
39913
|
const cmd = `git add ${JSON.stringify(f)}`;
|
|
39047
39914
|
displayStep(cmd);
|
|
39048
|
-
await
|
|
39915
|
+
await execAsync11(cmd);
|
|
39049
39916
|
}
|
|
39050
39917
|
console.log(source_default.dim(`
|
|
39051
39918
|
Staged ${files.length} file(s). Re-running analysis...
|
|
39052
39919
|
`));
|
|
39053
39920
|
const [newDiff, newStat] = await Promise.all([
|
|
39054
|
-
|
|
39055
|
-
|
|
39921
|
+
execAsync11("git diff --cached").then((r) => r.stdout),
|
|
39922
|
+
execAsync11("git diff --cached --stat").then((r) => r.stdout)
|
|
39056
39923
|
]);
|
|
39057
39924
|
const newAnalysis = await analyzeChanges(repoState.currentBranch, newDiff, newStat, recentCommits);
|
|
39058
39925
|
analysis.summary = newAnalysis.summary;
|
|
@@ -39087,7 +39954,7 @@ ${finalBody}` : finalMessage;
|
|
|
39087
39954
|
displayStep(`git commit -m "${finalMessage}"${finalBody ? " (with body)" : ""}`);
|
|
39088
39955
|
await execFileAsync("git", ["commit", "-m", fullMessage]);
|
|
39089
39956
|
displaySuccess("Committed!");
|
|
39090
|
-
const { stdout: hash } = await
|
|
39957
|
+
const { stdout: hash } = await execAsync11("git rev-parse --short HEAD");
|
|
39091
39958
|
console.log(source_default.dim(` ${hash.trim()} ${finalMessage}
|
|
39092
39959
|
`));
|
|
39093
39960
|
await recordCommand("commit", [], Date.now() - start, true);
|
|
@@ -39103,14 +39970,14 @@ ${finalBody}` : finalMessage;
|
|
|
39103
39970
|
}
|
|
39104
39971
|
|
|
39105
39972
|
// src/lib/update-notifier.ts
|
|
39106
|
-
import { readFile as
|
|
39107
|
-
import { existsSync as
|
|
39108
|
-
import { homedir as
|
|
39109
|
-
import
|
|
39973
|
+
import { readFile as readFile9, writeFile as writeFile8, mkdir as mkdir7 } from "fs/promises";
|
|
39974
|
+
import { existsSync as existsSync8 } from "fs";
|
|
39975
|
+
import { homedir as homedir4 } from "os";
|
|
39976
|
+
import path9 from "path";
|
|
39110
39977
|
var PACKAGE_NAME2 = "hermes-git";
|
|
39111
39978
|
var CHECK_INTERVAL = 12 * 60 * 60 * 1000;
|
|
39112
|
-
var
|
|
39113
|
-
var
|
|
39979
|
+
var CACHE_DIR2 = path9.join(homedir4(), ".hermes", "cache");
|
|
39980
|
+
var CACHE_FILE2 = path9.join(CACHE_DIR2, "update-check.json");
|
|
39114
39981
|
async function fetchDistTags() {
|
|
39115
39982
|
try {
|
|
39116
39983
|
const res = await fetch(`https://registry.npmjs.org/-/package/${PACKAGE_NAME2}/dist-tags`, { signal: AbortSignal.timeout(5000) });
|
|
@@ -39123,17 +39990,17 @@ async function fetchDistTags() {
|
|
|
39123
39990
|
}
|
|
39124
39991
|
async function readCache() {
|
|
39125
39992
|
try {
|
|
39126
|
-
if (!
|
|
39993
|
+
if (!existsSync8(CACHE_FILE2))
|
|
39127
39994
|
return null;
|
|
39128
|
-
return JSON.parse(await
|
|
39995
|
+
return JSON.parse(await readFile9(CACHE_FILE2, "utf-8"));
|
|
39129
39996
|
} catch {
|
|
39130
39997
|
return null;
|
|
39131
39998
|
}
|
|
39132
39999
|
}
|
|
39133
40000
|
async function writeCache(cache) {
|
|
39134
40001
|
try {
|
|
39135
|
-
await
|
|
39136
|
-
await
|
|
40002
|
+
await mkdir7(CACHE_DIR2, { recursive: true });
|
|
40003
|
+
await writeFile8(CACHE_FILE2, JSON.stringify(cache, null, 2));
|
|
39137
40004
|
} catch {}
|
|
39138
40005
|
}
|
|
39139
40006
|
function compareVersions2(a, b) {
|
|
@@ -39202,7 +40069,7 @@ async function checkForUpdates(currentVersion) {
|
|
|
39202
40069
|
}
|
|
39203
40070
|
|
|
39204
40071
|
// src/lib/banner.ts
|
|
39205
|
-
import { readFileSync, existsSync as
|
|
40072
|
+
import { readFileSync, existsSync as existsSync9 } from "fs";
|
|
39206
40073
|
var ART_LINES = [
|
|
39207
40074
|
"██╗ ██╗███████╗██████╗ ███╗ ███╗███████╗███████╗",
|
|
39208
40075
|
"██║ ██║██╔════╝██╔══██╗████╗ ████║██╔════╝██╔════╝",
|
|
@@ -39244,7 +40111,7 @@ var BUILTIN_WORKFLOWS = [
|
|
|
39244
40111
|
];
|
|
39245
40112
|
function loadProjectWorkflows() {
|
|
39246
40113
|
try {
|
|
39247
|
-
if (!
|
|
40114
|
+
if (!existsSync9(".hermes/config.json"))
|
|
39248
40115
|
return [];
|
|
39249
40116
|
const config = JSON.parse(readFileSync(".hermes/config.json", "utf-8"));
|
|
39250
40117
|
const workflows = config?.workflows;
|
|
@@ -39281,8 +40148,10 @@ function printWorkflows() {
|
|
|
39281
40148
|
}
|
|
39282
40149
|
|
|
39283
40150
|
// src/index.ts
|
|
40151
|
+
import { createRequire as createRequire2 } from "module";
|
|
39284
40152
|
var program2 = new Command;
|
|
39285
|
-
var
|
|
40153
|
+
var __require2 = createRequire2(import.meta.url);
|
|
40154
|
+
var { version: CURRENT_VERSION } = __require2("../package.json");
|
|
39286
40155
|
program2.name("hermes").description("Intent-driven Git, guided by AI").version(CURRENT_VERSION).action(() => {
|
|
39287
40156
|
printBanner(CURRENT_VERSION);
|
|
39288
40157
|
printWorkflows();
|