hermes-git 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -6
- package/dist/index.js +303 -194
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -469,25 +469,97 @@ Hermes is **terminal-native** and **explainable**:
|
|
|
469
469
|
### Copilot CLI Not Found
|
|
470
470
|
|
|
471
471
|
```bash
|
|
472
|
-
|
|
472
|
+
❌ GitHub Copilot CLI is not installed
|
|
473
473
|
```
|
|
474
474
|
|
|
475
|
-
**
|
|
475
|
+
**Problem:** Hermes requires the standalone Copilot CLI (not the deprecated `gh copilot` extension).
|
|
476
|
+
|
|
477
|
+
**Solution:**
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
# Install via npm (recommended)
|
|
481
|
+
npm install -g @github/copilot
|
|
482
|
+
|
|
483
|
+
# Or via Homebrew (macOS/Linux)
|
|
484
|
+
brew install github/gh/copilot-cli
|
|
485
|
+
|
|
486
|
+
# Verify installation
|
|
487
|
+
copilot --version
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**Note:** The old `gh copilot` extension was deprecated on October 25, 2025. If you have it installed, you'll see a warning—install the new standalone CLI instead.
|
|
491
|
+
|
|
492
|
+
---
|
|
476
493
|
|
|
477
494
|
### Authentication Required
|
|
478
495
|
|
|
479
496
|
```bash
|
|
480
|
-
|
|
497
|
+
❌ GitHub Copilot CLI is not authenticated
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
**Problem:** Copilot CLI needs GitHub authentication to work.
|
|
501
|
+
|
|
502
|
+
**Solution (choose one):**
|
|
503
|
+
|
|
504
|
+
**Option 1: OAuth (Recommended)**
|
|
505
|
+
```bash
|
|
506
|
+
# Start Copilot CLI interactively
|
|
507
|
+
copilot
|
|
508
|
+
|
|
509
|
+
# In the Copilot prompt, type:
|
|
510
|
+
/login
|
|
511
|
+
|
|
512
|
+
# Follow the browser OAuth flow
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
**Option 2: Personal Access Token**
|
|
516
|
+
```bash
|
|
517
|
+
# Create a token at: https://github.com/settings/tokens
|
|
518
|
+
# Enable "Copilot Requests" permission under "Permissions"
|
|
519
|
+
|
|
520
|
+
# Set the token in your environment
|
|
521
|
+
export GH_TOKEN="your_token_here"
|
|
522
|
+
|
|
523
|
+
# Add to your shell profile (~/.bashrc, ~/.zshrc) to persist:
|
|
524
|
+
echo 'export GH_TOKEN="your_token_here"' >> ~/.bashrc
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
**Option 3: GitHub CLI Integration**
|
|
528
|
+
```bash
|
|
529
|
+
# If you already have gh CLI installed and authenticated
|
|
530
|
+
gh auth login
|
|
531
|
+
|
|
532
|
+
# Copilot CLI will automatically use those credentials
|
|
481
533
|
```
|
|
482
534
|
|
|
483
|
-
**
|
|
535
|
+
**Requirements:**
|
|
536
|
+
- Active GitHub Copilot subscription ([subscribe here](https://github.com/features/copilot/plans))
|
|
537
|
+
- If using organization Copilot, "Copilot CLI" must be enabled in org settings
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
### Subscription Issues
|
|
542
|
+
|
|
543
|
+
```bash
|
|
544
|
+
❌ GitHub Copilot subscription required
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**Solution:**
|
|
548
|
+
- Verify you have an active Copilot subscription at [github.com/settings/copilot](https://github.com/settings/copilot)
|
|
549
|
+
- If using organization Copilot, check with your admin that "Copilot CLI" is enabled
|
|
550
|
+
- Individual plans start at $10/month, or free for students/OSS maintainers
|
|
551
|
+
|
|
552
|
+
---
|
|
484
553
|
|
|
485
554
|
### Slow Responses
|
|
486
555
|
|
|
487
556
|
If AI responses are slow, try:
|
|
488
|
-
- Use a faster model: `--model claude-sonnet-4.5` (default)
|
|
557
|
+
- Use a faster model: `--model claude-sonnet-4.5` (default, fastest)
|
|
489
558
|
- Check internet connection
|
|
490
559
|
- Verify Copilot subscription is active
|
|
560
|
+
- Try running `copilot` directly to test connection
|
|
561
|
+
|
|
562
|
+
---
|
|
491
563
|
|
|
492
564
|
### Command Not Working
|
|
493
565
|
|
|
@@ -495,16 +567,33 @@ If AI responses are slow, try:
|
|
|
495
567
|
# Check Hermes version
|
|
496
568
|
hermes --version
|
|
497
569
|
|
|
498
|
-
# Verify Copilot CLI
|
|
570
|
+
# Verify Copilot CLI is installed and authenticated
|
|
499
571
|
copilot --version
|
|
572
|
+
copilot -p "test" --allow-all-tools -s
|
|
573
|
+
|
|
574
|
+
# Check git is working
|
|
575
|
+
git status
|
|
500
576
|
|
|
501
577
|
# Test in a clean repo
|
|
502
578
|
cd /tmp && git init test && cd test
|
|
579
|
+
git config user.name "Test" && git config user.email "test@example.com"
|
|
503
580
|
hermes plan "test"
|
|
504
581
|
```
|
|
505
582
|
|
|
506
583
|
---
|
|
507
584
|
|
|
585
|
+
### Getting Help
|
|
586
|
+
|
|
587
|
+
If you're still having issues:
|
|
588
|
+
1. Check the [GitHub Copilot CLI documentation](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/use-copilot-cli)
|
|
589
|
+
2. Verify Copilot CLI works independently: `copilot -p "hello" -s`
|
|
590
|
+
3. Open an issue at [github.com/simandebvu/hermes-cli/issues](https://github.com/simandebvu/hermes-cli/issues) with:
|
|
591
|
+
- Output of `hermes --version`
|
|
592
|
+
- Output of `copilot --version`
|
|
593
|
+
- The exact error message you're seeing
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
508
597
|
## Contributing
|
|
509
598
|
|
|
510
599
|
Contributions are welcome! Please see [DEVELOPMENT.md](DEVELOPMENT.md) for:
|
package/dist/index.js
CHANGED
|
@@ -21939,200 +21939,6 @@ var {
|
|
|
21939
21939
|
// src/lib/copilot.ts
|
|
21940
21940
|
import { exec } from "child_process";
|
|
21941
21941
|
import { promisify } from "util";
|
|
21942
|
-
var execAsync = promisify(exec);
|
|
21943
|
-
async function getCopilotSuggestion(prompt, options = {}) {
|
|
21944
|
-
try {
|
|
21945
|
-
await checkCopilotCLI();
|
|
21946
|
-
const {
|
|
21947
|
-
model = "claude-sonnet-4.5",
|
|
21948
|
-
allowAllTools = true,
|
|
21949
|
-
silent = true
|
|
21950
|
-
} = options;
|
|
21951
|
-
const args = [
|
|
21952
|
-
"-p",
|
|
21953
|
-
`"${prompt.replace(/"/g, "\\\"")}"`,
|
|
21954
|
-
"--model",
|
|
21955
|
-
model
|
|
21956
|
-
];
|
|
21957
|
-
if (allowAllTools) {
|
|
21958
|
-
args.push("--allow-all-tools");
|
|
21959
|
-
}
|
|
21960
|
-
if (silent) {
|
|
21961
|
-
args.push("-s");
|
|
21962
|
-
}
|
|
21963
|
-
const command = `copilot ${args.join(" ")}`;
|
|
21964
|
-
const { stdout, stderr } = await execAsync(command, {
|
|
21965
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
21966
|
-
timeout: 120000
|
|
21967
|
-
});
|
|
21968
|
-
if (stderr && !stdout) {
|
|
21969
|
-
throw new Error(`Copilot CLI error: ${stderr}`);
|
|
21970
|
-
}
|
|
21971
|
-
return stdout.trim();
|
|
21972
|
-
} catch (error) {
|
|
21973
|
-
if (error.code === "ENOENT" || error.message.includes("not found")) {
|
|
21974
|
-
throw new Error(`GitHub Copilot CLI not found. Install it from: https://github.com/github/copilot-cli
|
|
21975
|
-
` + "Then authenticate with: copilot login");
|
|
21976
|
-
}
|
|
21977
|
-
throw new Error(`Copilot CLI error: ${error.message}`);
|
|
21978
|
-
}
|
|
21979
|
-
}
|
|
21980
|
-
async function checkCopilotCLI() {
|
|
21981
|
-
try {
|
|
21982
|
-
const { stdout } = await execAsync("copilot --version");
|
|
21983
|
-
if (!stdout.includes("GitHub Copilot CLI")) {
|
|
21984
|
-
throw new Error("Invalid Copilot CLI installation");
|
|
21985
|
-
}
|
|
21986
|
-
} catch (error) {
|
|
21987
|
-
if (error.code === "ENOENT") {
|
|
21988
|
-
throw new Error(`GitHub Copilot CLI is not installed.
|
|
21989
|
-
` + `Install from: https://github.com/github/copilot-cli
|
|
21990
|
-
` + "Or with npm: npm install -g @githubnext/github-copilot-cli");
|
|
21991
|
-
}
|
|
21992
|
-
throw error;
|
|
21993
|
-
}
|
|
21994
|
-
}
|
|
21995
|
-
async function analyzeCopilotGitState(repoState, intent) {
|
|
21996
|
-
const prompt = `
|
|
21997
|
-
You are a Git safety expert. Analyze this repository state and provide guidance.
|
|
21998
|
-
|
|
21999
|
-
Repository State:
|
|
22000
|
-
${JSON.stringify(repoState, null, 2)}
|
|
22001
|
-
|
|
22002
|
-
User Intent: "${intent}"
|
|
22003
|
-
|
|
22004
|
-
Provide a clear, actionable analysis including:
|
|
22005
|
-
1. Current state summary (clean/dirty, branch position, conflicts, etc.)
|
|
22006
|
-
2. Recommended approach with reasoning
|
|
22007
|
-
3. Potential risks and safety considerations
|
|
22008
|
-
4. Step-by-step Git commands (if applicable)
|
|
22009
|
-
|
|
22010
|
-
Be specific about WHY each step is safe and necessary.
|
|
22011
|
-
Format your response in clear sections.
|
|
22012
|
-
`;
|
|
22013
|
-
return getCopilotSuggestion(prompt);
|
|
22014
|
-
}
|
|
22015
|
-
async function getCopilotGitPlan(repoState, intent, outputFormat = "json") {
|
|
22016
|
-
const formatInstruction = outputFormat === "json" ? "Return your response as RAW JSON ONLY (no markdown code blocks, no backticks, just pure JSON) with fields: explanation, commands[], risks[], safetyNotes[]" : "Return your response as formatted text with clear sections";
|
|
22017
|
-
const prompt = `
|
|
22018
|
-
You are a Git automation expert. Create a safe execution plan.
|
|
22019
|
-
|
|
22020
|
-
Repository State:
|
|
22021
|
-
${JSON.stringify(repoState, null, 2)}
|
|
22022
|
-
|
|
22023
|
-
User wants to: "${intent}"
|
|
22024
|
-
|
|
22025
|
-
${formatInstruction}
|
|
22026
|
-
|
|
22027
|
-
Ensure all Git commands are:
|
|
22028
|
-
- Safe (non-destructive when possible)
|
|
22029
|
-
- Ordered correctly
|
|
22030
|
-
- Include necessary error handling
|
|
22031
|
-
- Explain the purpose of each command
|
|
22032
|
-
`;
|
|
22033
|
-
const response = await getCopilotSuggestion(prompt);
|
|
22034
|
-
if (outputFormat === "json") {
|
|
22035
|
-
return stripMarkdownCodeBlock(response);
|
|
22036
|
-
}
|
|
22037
|
-
return response;
|
|
22038
|
-
}
|
|
22039
|
-
function stripMarkdownCodeBlock(text) {
|
|
22040
|
-
const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
22041
|
-
if (codeBlockMatch) {
|
|
22042
|
-
return codeBlockMatch[1].trim();
|
|
22043
|
-
}
|
|
22044
|
-
return text.trim();
|
|
22045
|
-
}
|
|
22046
|
-
|
|
22047
|
-
// src/lib/git.ts
|
|
22048
|
-
import { exec as exec2 } from "child_process";
|
|
22049
|
-
import { promisify as promisify2 } from "util";
|
|
22050
|
-
var execAsync2 = promisify2(exec2);
|
|
22051
|
-
async function getRepoState() {
|
|
22052
|
-
try {
|
|
22053
|
-
const [
|
|
22054
|
-
currentBranch,
|
|
22055
|
-
status,
|
|
22056
|
-
tracking,
|
|
22057
|
-
rebaseStatus,
|
|
22058
|
-
mergeStatus,
|
|
22059
|
-
cherryPickStatus
|
|
22060
|
-
] = await Promise.all([
|
|
22061
|
-
execAsync2("git rev-parse --abbrev-ref HEAD").then((r) => r.stdout.trim()),
|
|
22062
|
-
execAsync2("git status --porcelain").then((r) => r.stdout.trim()),
|
|
22063
|
-
execAsync2("git rev-parse --abbrev-ref @{upstream}").then((r) => r.stdout.trim()).catch(() => null),
|
|
22064
|
-
execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -d ${r.stdout.trim()}/rebase-merge`)).then(() => true).catch(() => false),
|
|
22065
|
-
execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -f ${r.stdout.trim()}/MERGE_HEAD`)).then(() => true).catch(() => false),
|
|
22066
|
-
execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -f ${r.stdout.trim()}/CHERRY_PICK_HEAD`)).then(() => true).catch(() => false)
|
|
22067
|
-
]);
|
|
22068
|
-
const statusLines = status.split(`
|
|
22069
|
-
`).filter((line) => line);
|
|
22070
|
-
const hasUncommittedChanges = statusLines.some((line) => line.startsWith(" M") || line.startsWith("M "));
|
|
22071
|
-
const hasUntrackedFiles = statusLines.some((line) => line.startsWith("??"));
|
|
22072
|
-
let ahead = 0;
|
|
22073
|
-
let behind = 0;
|
|
22074
|
-
if (tracking) {
|
|
22075
|
-
const aheadBehind = await execAsync2(`git rev-list --left-right --count ${tracking}...HEAD`).then((r) => r.stdout.trim().split("\t")).catch(() => ["0", "0"]);
|
|
22076
|
-
behind = parseInt(aheadBehind[0], 10);
|
|
22077
|
-
ahead = parseInt(aheadBehind[1], 10);
|
|
22078
|
-
}
|
|
22079
|
-
return {
|
|
22080
|
-
currentBranch,
|
|
22081
|
-
isClean: statusLines.length === 0,
|
|
22082
|
-
hasUncommittedChanges,
|
|
22083
|
-
hasUntrackedFiles,
|
|
22084
|
-
isInRebase: rebaseStatus,
|
|
22085
|
-
isInMerge: mergeStatus,
|
|
22086
|
-
isInCherryPick: cherryPickStatus,
|
|
22087
|
-
remoteTracking: tracking || undefined,
|
|
22088
|
-
ahead,
|
|
22089
|
-
behind
|
|
22090
|
-
};
|
|
22091
|
-
} catch (error) {
|
|
22092
|
-
throw new Error("Not a Git repository or Git is not installed");
|
|
22093
|
-
}
|
|
22094
|
-
}
|
|
22095
|
-
async function isGitInstalled() {
|
|
22096
|
-
try {
|
|
22097
|
-
await execAsync2("git --version");
|
|
22098
|
-
return true;
|
|
22099
|
-
} catch {
|
|
22100
|
-
return false;
|
|
22101
|
-
}
|
|
22102
|
-
}
|
|
22103
|
-
async function isGitRepository() {
|
|
22104
|
-
try {
|
|
22105
|
-
await execAsync2("git rev-parse --git-dir");
|
|
22106
|
-
return true;
|
|
22107
|
-
} catch {
|
|
22108
|
-
return false;
|
|
22109
|
-
}
|
|
22110
|
-
}
|
|
22111
|
-
async function hasCommits() {
|
|
22112
|
-
try {
|
|
22113
|
-
await execAsync2("git rev-parse HEAD");
|
|
22114
|
-
return true;
|
|
22115
|
-
} catch {
|
|
22116
|
-
return false;
|
|
22117
|
-
}
|
|
22118
|
-
}
|
|
22119
|
-
async function executeGitCommand(command) {
|
|
22120
|
-
try {
|
|
22121
|
-
const { stdout, stderr } = await execAsync2(command);
|
|
22122
|
-
return stdout || stderr;
|
|
22123
|
-
} catch (error) {
|
|
22124
|
-
throw new Error(`Git command failed: ${error.message}`);
|
|
22125
|
-
}
|
|
22126
|
-
}
|
|
22127
|
-
async function getConflictedFiles() {
|
|
22128
|
-
try {
|
|
22129
|
-
const { stdout } = await execAsync2("git diff --name-only --diff-filter=U");
|
|
22130
|
-
return stdout.trim().split(`
|
|
22131
|
-
`).filter((line) => line);
|
|
22132
|
-
} catch {
|
|
22133
|
-
return [];
|
|
22134
|
-
}
|
|
22135
|
-
}
|
|
22136
21942
|
|
|
22137
21943
|
// node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
22138
21944
|
var ANSI_BACKGROUND_OFFSET = 10;
|
|
@@ -22623,6 +22429,309 @@ var chalk = createChalk();
|
|
|
22623
22429
|
var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
22624
22430
|
var source_default = chalk;
|
|
22625
22431
|
|
|
22432
|
+
// src/lib/copilot.ts
|
|
22433
|
+
var execAsync = promisify(exec);
|
|
22434
|
+
async function getCopilotSuggestion(prompt, options = {}) {
|
|
22435
|
+
try {
|
|
22436
|
+
await checkCopilotCLI();
|
|
22437
|
+
const {
|
|
22438
|
+
model = "claude-sonnet-4.5",
|
|
22439
|
+
allowAllTools = true,
|
|
22440
|
+
silent = true
|
|
22441
|
+
} = options;
|
|
22442
|
+
const args = [
|
|
22443
|
+
"-p",
|
|
22444
|
+
`"${prompt.replace(/"/g, "\\\"")}"`,
|
|
22445
|
+
"--model",
|
|
22446
|
+
model
|
|
22447
|
+
];
|
|
22448
|
+
if (allowAllTools) {
|
|
22449
|
+
args.push("--allow-all-tools");
|
|
22450
|
+
}
|
|
22451
|
+
if (silent) {
|
|
22452
|
+
args.push("-s");
|
|
22453
|
+
}
|
|
22454
|
+
const command = `copilot ${args.join(" ")}`;
|
|
22455
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
22456
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
22457
|
+
timeout: 120000
|
|
22458
|
+
});
|
|
22459
|
+
if (stderr && !stdout) {
|
|
22460
|
+
throw new Error(`Copilot CLI error: ${stderr}`);
|
|
22461
|
+
}
|
|
22462
|
+
return stdout.trim();
|
|
22463
|
+
} catch (error) {
|
|
22464
|
+
if (error.code === "ENOENT" || error.message.includes("not found")) {
|
|
22465
|
+
throw new CopilotNotInstalledError;
|
|
22466
|
+
}
|
|
22467
|
+
const errorMessage = (error.message || "").toLowerCase();
|
|
22468
|
+
const stderr = (error.stderr || "").toLowerCase();
|
|
22469
|
+
const combinedError = errorMessage + " " + stderr;
|
|
22470
|
+
if (combinedError.includes("no authentication information") || combinedError.includes("authentication required") || combinedError.includes("not authenticated") || combinedError.includes("copilot_github_token") || combinedError.includes("gh_token") || combinedError.includes("github_token")) {
|
|
22471
|
+
throw new CopilotNotAuthenticatedError(error.message);
|
|
22472
|
+
}
|
|
22473
|
+
if (combinedError.includes("subscription") || combinedError.includes("not authorized") || combinedError.includes("access denied")) {
|
|
22474
|
+
throw new Error(source_default.red(`❌ GitHub Copilot subscription required
|
|
22475
|
+
|
|
22476
|
+
`) + source_default.white(`Hermes requires an active GitHub Copilot subscription.
|
|
22477
|
+
`) + source_default.gray(`Subscribe at: https://github.com/features/copilot/plans
|
|
22478
|
+
|
|
22479
|
+
`) + source_default.dim("Original error: " + error.message));
|
|
22480
|
+
}
|
|
22481
|
+
throw new Error(source_default.red(`❌ Copilot CLI error
|
|
22482
|
+
|
|
22483
|
+
`) + source_default.white(error.message) + `
|
|
22484
|
+
|
|
22485
|
+
` + source_default.dim("If this persists, try running: copilot --version"));
|
|
22486
|
+
}
|
|
22487
|
+
}
|
|
22488
|
+
async function checkCopilotCLI() {
|
|
22489
|
+
await checkForDeprecatedGhCopilot();
|
|
22490
|
+
try {
|
|
22491
|
+
const { stdout } = await execAsync("copilot --version", { timeout: 5000 });
|
|
22492
|
+
if (!stdout.includes("GitHub Copilot CLI")) {
|
|
22493
|
+
throw new Error("Invalid Copilot CLI installation");
|
|
22494
|
+
}
|
|
22495
|
+
} catch (error) {
|
|
22496
|
+
if (error.code === "ENOENT" || error.message.includes("not found")) {
|
|
22497
|
+
throw new CopilotNotInstalledError;
|
|
22498
|
+
}
|
|
22499
|
+
throw error;
|
|
22500
|
+
}
|
|
22501
|
+
}
|
|
22502
|
+
async function checkForDeprecatedGhCopilot() {
|
|
22503
|
+
try {
|
|
22504
|
+
const { stdout } = await execAsync("gh copilot --version", { timeout: 5000 });
|
|
22505
|
+
if (stdout) {
|
|
22506
|
+
console.warn(source_default.yellow(`
|
|
22507
|
+
⚠️ Warning: You have the deprecated "gh copilot" extension installed.`));
|
|
22508
|
+
console.warn(source_default.yellow(" The GitHub Copilot extension was deprecated on October 25, 2025."));
|
|
22509
|
+
console.warn(source_default.yellow(` Hermes requires the new standalone "copilot" CLI.
|
|
22510
|
+
`));
|
|
22511
|
+
}
|
|
22512
|
+
} catch {}
|
|
22513
|
+
}
|
|
22514
|
+
|
|
22515
|
+
class CopilotNotInstalledError extends Error {
|
|
22516
|
+
constructor() {
|
|
22517
|
+
const message = [
|
|
22518
|
+
"",
|
|
22519
|
+
source_default.red("❌ GitHub Copilot CLI is not installed"),
|
|
22520
|
+
"",
|
|
22521
|
+
source_default.bold("Hermes requires the standalone Copilot CLI (not the deprecated gh copilot extension)."),
|
|
22522
|
+
"",
|
|
22523
|
+
source_default.cyan("\uD83D\uDCE6 Installation Options:"),
|
|
22524
|
+
"",
|
|
22525
|
+
source_default.white(" npm: ") + source_default.gray("npm install -g @github/copilot"),
|
|
22526
|
+
source_default.white(" Homebrew: ") + source_default.gray("brew install github/gh/copilot-cli"),
|
|
22527
|
+
source_default.white(" Manual: ") + source_default.gray("https://github.com/github/copilot-cli/releases"),
|
|
22528
|
+
"",
|
|
22529
|
+
source_default.cyan("\uD83D\uDD10 After installation, authenticate with:"),
|
|
22530
|
+
"",
|
|
22531
|
+
source_default.white(" copilot") + source_default.gray(" (then run /login command)"),
|
|
22532
|
+
"",
|
|
22533
|
+
source_default.dim("\uD83D\uDCDA Learn more: https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli"),
|
|
22534
|
+
""
|
|
22535
|
+
].join(`
|
|
22536
|
+
`);
|
|
22537
|
+
super(message);
|
|
22538
|
+
this.name = "CopilotNotInstalledError";
|
|
22539
|
+
}
|
|
22540
|
+
}
|
|
22541
|
+
|
|
22542
|
+
class CopilotNotAuthenticatedError extends Error {
|
|
22543
|
+
constructor(originalError) {
|
|
22544
|
+
const ghAuthStatus = checkGhAuthenticationSync();
|
|
22545
|
+
const message = [
|
|
22546
|
+
"",
|
|
22547
|
+
source_default.red("❌ GitHub Copilot CLI is not authenticated"),
|
|
22548
|
+
"",
|
|
22549
|
+
ghAuthStatus.isAuthenticated ? source_default.yellow("⚠️ Note: You have gh CLI authenticated, but Copilot CLI needs separate authentication.") : source_default.bold("Hermes needs access to GitHub Copilot to provide AI-powered Git guidance."),
|
|
22550
|
+
"",
|
|
22551
|
+
source_default.cyan("\uD83D\uDD10 Quick Fix:"),
|
|
22552
|
+
"",
|
|
22553
|
+
source_default.white(" Run: ") + source_default.bold.green("copilot"),
|
|
22554
|
+
source_default.gray(" Then type: ") + source_default.bold("/login"),
|
|
22555
|
+
source_default.gray(" Follow the OAuth flow in your browser"),
|
|
22556
|
+
"",
|
|
22557
|
+
source_default.cyan("\uD83D\uDD10 Alternative - Personal Access Token:"),
|
|
22558
|
+
"",
|
|
22559
|
+
source_default.gray(" 1. Create token: https://github.com/settings/tokens/new"),
|
|
22560
|
+
source_default.gray(' 2. Enable "Copilot Requests" permission'),
|
|
22561
|
+
source_default.gray(" 3. Run: ") + source_default.white('export GH_TOKEN="your_token_here"'),
|
|
22562
|
+
source_default.gray(" 4. Add to ~/.bashrc or ~/.zshrc to persist"),
|
|
22563
|
+
"",
|
|
22564
|
+
source_default.cyan("\uD83D\uDCCB Requirements:"),
|
|
22565
|
+
source_default.gray(" • Active GitHub Copilot subscription"),
|
|
22566
|
+
source_default.gray(' • If using organization Copilot, "Copilot CLI" must be enabled in org settings'),
|
|
22567
|
+
"",
|
|
22568
|
+
source_default.dim("\uD83D\uDCDA Learn more: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/use-copilot-cli"),
|
|
22569
|
+
""
|
|
22570
|
+
].join(`
|
|
22571
|
+
`);
|
|
22572
|
+
super(message);
|
|
22573
|
+
this.name = "CopilotNotAuthenticatedError";
|
|
22574
|
+
}
|
|
22575
|
+
}
|
|
22576
|
+
function checkGhAuthenticationSync() {
|
|
22577
|
+
try {
|
|
22578
|
+
const { execSync } = __require("child_process");
|
|
22579
|
+
const result = execSync("gh auth status 2>&1", {
|
|
22580
|
+
encoding: "utf-8",
|
|
22581
|
+
timeout: 2000
|
|
22582
|
+
});
|
|
22583
|
+
const isAuthenticated = result.includes("Logged in to github.com");
|
|
22584
|
+
const usernameMatch = result.match(/Logged in to github\.com as (\S+)/);
|
|
22585
|
+
return {
|
|
22586
|
+
isAuthenticated,
|
|
22587
|
+
username: usernameMatch?.[1]
|
|
22588
|
+
};
|
|
22589
|
+
} catch {
|
|
22590
|
+
return { isAuthenticated: false };
|
|
22591
|
+
}
|
|
22592
|
+
}
|
|
22593
|
+
async function analyzeCopilotGitState(repoState, intent) {
|
|
22594
|
+
const prompt = `
|
|
22595
|
+
You are a Git safety expert. Analyze this repository state and provide guidance.
|
|
22596
|
+
|
|
22597
|
+
Repository State:
|
|
22598
|
+
${JSON.stringify(repoState, null, 2)}
|
|
22599
|
+
|
|
22600
|
+
User Intent: "${intent}"
|
|
22601
|
+
|
|
22602
|
+
Provide a clear, actionable analysis including:
|
|
22603
|
+
1. Current state summary (clean/dirty, branch position, conflicts, etc.)
|
|
22604
|
+
2. Recommended approach with reasoning
|
|
22605
|
+
3. Potential risks and safety considerations
|
|
22606
|
+
4. Step-by-step Git commands (if applicable)
|
|
22607
|
+
|
|
22608
|
+
Be specific about WHY each step is safe and necessary.
|
|
22609
|
+
Format your response in clear sections.
|
|
22610
|
+
`;
|
|
22611
|
+
return getCopilotSuggestion(prompt);
|
|
22612
|
+
}
|
|
22613
|
+
async function getCopilotGitPlan(repoState, intent, outputFormat = "json") {
|
|
22614
|
+
const formatInstruction = outputFormat === "json" ? "Return your response as RAW JSON ONLY (no markdown code blocks, no backticks, just pure JSON) with fields: explanation, commands[], risks[], safetyNotes[]" : "Return your response as formatted text with clear sections";
|
|
22615
|
+
const prompt = `
|
|
22616
|
+
You are a Git automation expert. Create a safe execution plan.
|
|
22617
|
+
|
|
22618
|
+
Repository State:
|
|
22619
|
+
${JSON.stringify(repoState, null, 2)}
|
|
22620
|
+
|
|
22621
|
+
User wants to: "${intent}"
|
|
22622
|
+
|
|
22623
|
+
${formatInstruction}
|
|
22624
|
+
|
|
22625
|
+
Ensure all Git commands are:
|
|
22626
|
+
- Safe (non-destructive when possible)
|
|
22627
|
+
- Ordered correctly
|
|
22628
|
+
- Include necessary error handling
|
|
22629
|
+
- Explain the purpose of each command
|
|
22630
|
+
`;
|
|
22631
|
+
const response = await getCopilotSuggestion(prompt);
|
|
22632
|
+
if (outputFormat === "json") {
|
|
22633
|
+
return stripMarkdownCodeBlock(response);
|
|
22634
|
+
}
|
|
22635
|
+
return response;
|
|
22636
|
+
}
|
|
22637
|
+
function stripMarkdownCodeBlock(text) {
|
|
22638
|
+
const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
22639
|
+
if (codeBlockMatch) {
|
|
22640
|
+
return codeBlockMatch[1].trim();
|
|
22641
|
+
}
|
|
22642
|
+
return text.trim();
|
|
22643
|
+
}
|
|
22644
|
+
|
|
22645
|
+
// src/lib/git.ts
|
|
22646
|
+
import { exec as exec2 } from "child_process";
|
|
22647
|
+
import { promisify as promisify2 } from "util";
|
|
22648
|
+
var execAsync2 = promisify2(exec2);
|
|
22649
|
+
async function getRepoState() {
|
|
22650
|
+
try {
|
|
22651
|
+
const [
|
|
22652
|
+
currentBranch,
|
|
22653
|
+
status,
|
|
22654
|
+
tracking,
|
|
22655
|
+
rebaseStatus,
|
|
22656
|
+
mergeStatus,
|
|
22657
|
+
cherryPickStatus
|
|
22658
|
+
] = await Promise.all([
|
|
22659
|
+
execAsync2("git rev-parse --abbrev-ref HEAD").then((r) => r.stdout.trim()),
|
|
22660
|
+
execAsync2("git status --porcelain").then((r) => r.stdout.trim()),
|
|
22661
|
+
execAsync2("git rev-parse --abbrev-ref @{upstream}").then((r) => r.stdout.trim()).catch(() => null),
|
|
22662
|
+
execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -d ${r.stdout.trim()}/rebase-merge`)).then(() => true).catch(() => false),
|
|
22663
|
+
execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -f ${r.stdout.trim()}/MERGE_HEAD`)).then(() => true).catch(() => false),
|
|
22664
|
+
execAsync2("git rev-parse --git-dir").then((r) => execAsync2(`test -f ${r.stdout.trim()}/CHERRY_PICK_HEAD`)).then(() => true).catch(() => false)
|
|
22665
|
+
]);
|
|
22666
|
+
const statusLines = status.split(`
|
|
22667
|
+
`).filter((line) => line);
|
|
22668
|
+
const hasUncommittedChanges = statusLines.some((line) => line.startsWith(" M") || line.startsWith("M "));
|
|
22669
|
+
const hasUntrackedFiles = statusLines.some((line) => line.startsWith("??"));
|
|
22670
|
+
let ahead = 0;
|
|
22671
|
+
let behind = 0;
|
|
22672
|
+
if (tracking) {
|
|
22673
|
+
const aheadBehind = await execAsync2(`git rev-list --left-right --count ${tracking}...HEAD`).then((r) => r.stdout.trim().split("\t")).catch(() => ["0", "0"]);
|
|
22674
|
+
behind = parseInt(aheadBehind[0], 10);
|
|
22675
|
+
ahead = parseInt(aheadBehind[1], 10);
|
|
22676
|
+
}
|
|
22677
|
+
return {
|
|
22678
|
+
currentBranch,
|
|
22679
|
+
isClean: statusLines.length === 0,
|
|
22680
|
+
hasUncommittedChanges,
|
|
22681
|
+
hasUntrackedFiles,
|
|
22682
|
+
isInRebase: rebaseStatus,
|
|
22683
|
+
isInMerge: mergeStatus,
|
|
22684
|
+
isInCherryPick: cherryPickStatus,
|
|
22685
|
+
remoteTracking: tracking || undefined,
|
|
22686
|
+
ahead,
|
|
22687
|
+
behind
|
|
22688
|
+
};
|
|
22689
|
+
} catch (error) {
|
|
22690
|
+
throw new Error("Not a Git repository or Git is not installed");
|
|
22691
|
+
}
|
|
22692
|
+
}
|
|
22693
|
+
async function isGitInstalled() {
|
|
22694
|
+
try {
|
|
22695
|
+
await execAsync2("git --version");
|
|
22696
|
+
return true;
|
|
22697
|
+
} catch {
|
|
22698
|
+
return false;
|
|
22699
|
+
}
|
|
22700
|
+
}
|
|
22701
|
+
async function isGitRepository() {
|
|
22702
|
+
try {
|
|
22703
|
+
await execAsync2("git rev-parse --git-dir");
|
|
22704
|
+
return true;
|
|
22705
|
+
} catch {
|
|
22706
|
+
return false;
|
|
22707
|
+
}
|
|
22708
|
+
}
|
|
22709
|
+
async function hasCommits() {
|
|
22710
|
+
try {
|
|
22711
|
+
await execAsync2("git rev-parse HEAD");
|
|
22712
|
+
return true;
|
|
22713
|
+
} catch {
|
|
22714
|
+
return false;
|
|
22715
|
+
}
|
|
22716
|
+
}
|
|
22717
|
+
async function executeGitCommand(command) {
|
|
22718
|
+
try {
|
|
22719
|
+
const { stdout, stderr } = await execAsync2(command);
|
|
22720
|
+
return stdout || stderr;
|
|
22721
|
+
} catch (error) {
|
|
22722
|
+
throw new Error(`Git command failed: ${error.message}`);
|
|
22723
|
+
}
|
|
22724
|
+
}
|
|
22725
|
+
async function getConflictedFiles() {
|
|
22726
|
+
try {
|
|
22727
|
+
const { stdout } = await execAsync2("git diff --name-only --diff-filter=U");
|
|
22728
|
+
return stdout.trim().split(`
|
|
22729
|
+
`).filter((line) => line);
|
|
22730
|
+
} catch {
|
|
22731
|
+
return [];
|
|
22732
|
+
}
|
|
22733
|
+
}
|
|
22734
|
+
|
|
22626
22735
|
// src/lib/display.ts
|
|
22627
22736
|
function displayPlan(suggestion, repoState) {
|
|
22628
22737
|
console.log(source_default.bold.cyan(`\uD83D\uDCCB Recommended Plan:
|