mirror of
https://github.com/prdlk/gh-commit.git
synced 2026-08-02 17:31:40 +00:00
fix: Refactor to use crush
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# gh-commit
|
# gh-commit
|
||||||
|
|
||||||
AI-powered scoped git commits. Groups changes by project area, generates commit messages with Claude, and pushes — all in one command. A single Claude agent, reached over the **Agent Client Protocol (ACP)**, handles both scope generation and commit-message writing.
|
AI-powered scoped git commits. Groups changes by project area, generates commit messages with `crush run`, and pushes — all in one command.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@@ -11,15 +11,14 @@ gh extension install prdlk/gh-commit
|
|||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- [uv](https://docs.astral.sh/uv) — Python package runner (handles dependencies automatically)
|
- [uv](https://docs.astral.sh/uv) — Python package runner (handles dependencies automatically)
|
||||||
- [Node.js](https://nodejs.org) — runs the Claude ACP bridge via `npx`
|
- [Crush](https://github.com/charmbracelet/crush) — configured with an authenticated model provider
|
||||||
- [claude](https://docs.anthropic.com/en/docs/claude-code) — authenticated Claude Code CLI (the ACP bridge drives it)
|
|
||||||
|
|
||||||
> The ACP bridge defaults to `npx --yes @zed-industries/claude-code-acp`. Override it with `GH_COMMIT_ACP_CMD` if you prefer another bridge.
|
By default, gh-commit uses `openrouter/qwen/qwen3.6-27b`. Configure OpenRouter in Crush or override the model with `GH_COMMIT_CRUSH_MODEL`.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Initialize scopes for your repo (uses Claude to analyze structure)
|
# Initialize scopes for your repo (uses Crush to analyze structure)
|
||||||
gh commit init
|
gh commit init
|
||||||
|
|
||||||
# Commit changes grouped by scope
|
# Commit changes grouped by scope
|
||||||
@@ -37,17 +36,17 @@ gh commit sync
|
|||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
1. **`gh commit init`** — A Claude agent (over ACP) analyzes your repo structure and generates scope definitions (e.g., `core → src/`, `docs → docs/, README.md`, `ci → .github/workflows/`)
|
1. **`gh commit init`** — `crush run` analyzes your repo structure and generates scope definitions (e.g., `core → src/`, `docs → docs/, README.md`, `ci → .github/workflows/`)
|
||||||
2. **`gh commit`** — Groups dirty files by scope, generates a commit message per scope via the same Claude agent, and commits each group separately
|
2. **`gh commit`** — Groups dirty files by scope, sends each staged diff to `crush run`, and commits each group separately
|
||||||
3. **Auto-refresh** — Whenever your `.gitignore` changes, scopes are automatically regenerated before committing (a content hash of `.gitignore` is tracked per repo)
|
3. **Auto-refresh** — Whenever your `.gitignore` changes, scopes are automatically regenerated before committing (a content hash of `.gitignore` is tracked per repo)
|
||||||
4. Remaining unscoped files are handled in a final pass
|
4. Remaining unscoped files are handled in a final pass
|
||||||
5. Unpushed commits are offered for push
|
5. Unpushed commits are offered for push
|
||||||
|
|
||||||
Scopes are stored in a local DuckDB database (`~/.local/share/gh-commit/gh-commit.db`) — no config files in your repo.
|
Scopes are stored in a local DuckDB database (`~/.local/share/gh-commit/gh-commit.db`) — no config files in your repo.
|
||||||
|
|
||||||
### Why ACP?
|
### Why Crush?
|
||||||
|
|
||||||
gh-commit talks to Claude through the [Agent Client Protocol](https://agentclientprotocol.com) — the same protocol editors like Zed use to drive coding agents. One Claude backend handles everything (no separate `mods`/LLM CLI), the bridge process is reused across a run, and each generation runs in its own session so context never bleeds between commits.
|
The former Mods roles now live directly in `smartcommit.py`, so gh-commit no longer depends on Mods configuration. Each generation invokes Crush's supported non-interactive mode with a self-contained prompt.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -77,10 +76,10 @@ gh-commit talks to Claude through the [Agent Client Protocol](https://agentclien
|
|||||||
| `GH_COMMIT_AUTO=1` | Skip all confirmation prompts |
|
| `GH_COMMIT_AUTO=1` | Skip all confirmation prompts |
|
||||||
| `GH_COMMIT_PUSH=1` | Auto-push after commits |
|
| `GH_COMMIT_PUSH=1` | Auto-push after commits |
|
||||||
| `GH_COMMIT_NO_AUTO_REFRESH=1` | Don't auto-regenerate scopes when `.gitignore` changes |
|
| `GH_COMMIT_NO_AUTO_REFRESH=1` | Don't auto-regenerate scopes when `.gitignore` changes |
|
||||||
| `GH_COMMIT_ACP_CMD` | Override the Claude ACP bridge command (default `npx --yes @zed-industries/claude-code-acp`) |
|
| `GH_COMMIT_CRUSH_CMD` | Override the Crush command (default `crush`) |
|
||||||
| `GH_COMMIT_ACP_PERMISSION` | ACP permission mode (default `bypassPermissions`) |
|
| `GH_COMMIT_CRUSH_MODEL` | Override the Crush model (default `openrouter/qwen/qwen3.6-27b`) |
|
||||||
| `GH_COMMIT_ACP_TIMEOUT` | Per-prompt timeout in seconds (default `300`) |
|
| `GH_COMMIT_CRUSH_TIMEOUT` | Per-prompt timeout in seconds (default `120`) |
|
||||||
| `GH_COMMIT_DEBUG=1` | Show ACP bridge stderr and parse diagnostics |
|
| `GH_COMMIT_DEBUG=1` | Show scope-response parse diagnostics |
|
||||||
|
|
||||||
## Migration
|
## Migration
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# gh-commit: AI-powered scoped git commits (mods CLI for scopes and messages)
|
# gh-commit: AI-powered scoped git commits (Crush for scopes and messages)
|
||||||
# Install: gh extension install prdlk/gh-commit
|
# Install: gh extension install prdlk/gh-commit
|
||||||
# Requires: uv (https://docs.astral.sh/uv) and the `mods` CLI
|
# Requires: uv (https://docs.astral.sh/uv) and the Crush CLI
|
||||||
# (https://github.com/charmbracelet/mods)
|
# (https://github.com/charmbracelet/crush)
|
||||||
|
|
||||||
EXTENSION_DIR="$(cd "$(dirname "$0")" && pwd)"
|
EXTENSION_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
SCRIPT="${EXTENSION_DIR}/smartcommit.py"
|
SCRIPT="${EXTENSION_DIR}/smartcommit.py"
|
||||||
@@ -15,9 +15,9 @@ if ! command -v uv &>/dev/null; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! command -v mods &>/dev/null; then
|
if [[ -z "${GH_COMMIT_CRUSH_CMD:-}" ]] && ! command -v crush &>/dev/null; then
|
||||||
echo "warning: 'mods' not found — gh-commit uses it for scopes and commit messages."
|
echo "warning: 'crush' not found — gh-commit uses 'crush run' for scopes and commit messages."
|
||||||
echo "install mods (https://github.com/charmbracelet/mods), or set GH_COMMIT_MODS_CMD."
|
echo "install Crush (https://github.com/charmbracelet/crush), or set GH_COMMIT_CRUSH_CMD."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec uv run --script "${SCRIPT}" "$@"
|
exec uv run --script "${SCRIPT}" "$@"
|
||||||
|
|||||||
Binary file not shown.
+73
-69
@@ -9,10 +9,10 @@
|
|||||||
# ///
|
# ///
|
||||||
"""gh-commit - AI-powered scoped git commits.
|
"""gh-commit - AI-powered scoped git commits.
|
||||||
|
|
||||||
Both AI steps run through the `mods` CLI, so the tool never touches Claude's
|
Both AI steps run through `crush run`: scope generation sends the repository's
|
||||||
ACP credit path: scope generation pipes the repository's file tree into a
|
file tree with embedded scope-identification instructions, and commit-message
|
||||||
`scope-identifier` role, and commit-message writing pipes the staged diff into
|
writing sends the staged diff with embedded Conventional Commits instructions.
|
||||||
a `write-commit` role. Scopes live in a local DuckDB and auto-regenerate
|
Scopes live in a local DuckDB and auto-regenerate
|
||||||
whenever the repository's .gitignore changes.
|
whenever the repository's .gitignore changes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -41,15 +41,11 @@ console = Console()
|
|||||||
DB_DIR = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local/share")) / "gh-commit"
|
DB_DIR = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local/share")) / "gh-commit"
|
||||||
DB_PATH = DB_DIR / "gh-commit.db"
|
DB_PATH = DB_DIR / "gh-commit.db"
|
||||||
|
|
||||||
# The `mods` CLI handles both AI steps (a prompt is piped to a predefined role),
|
# Crush handles both AI steps in non-interactive mode. The instructions formerly
|
||||||
# keeping the tool off Claude's ACP credit path.
|
# stored as mods roles are embedded below so this tool is self-contained.
|
||||||
MODS_CMD = os.environ.get("GH_COMMIT_MODS_CMD", "mods").split()
|
CRUSH_CMD = os.environ.get("GH_COMMIT_CRUSH_CMD", "crush").split()
|
||||||
MODS_SCOPE_ROLE = os.environ.get("GH_COMMIT_MODS_SCOPE_ROLE", "scope-identifier")
|
CRUSH_MODEL = os.environ.get("GH_COMMIT_CRUSH_MODEL", "openrouter/qwen/qwen3.6-27b")
|
||||||
MODS_COMMIT_ROLE = os.environ.get("GH_COMMIT_MODS_COMMIT_ROLE", "write-commit")
|
CRUSH_TIMEOUT = int(os.environ.get("GH_COMMIT_CRUSH_TIMEOUT", "120"))
|
||||||
MODS_API = os.environ.get("GH_COMMIT_MODS_API", "openrouter")
|
|
||||||
MODS_MODEL = os.environ.get("GH_COMMIT_MODS_MODEL", "qwen/qwen3.6-27b")
|
|
||||||
MODS_MAX_TOKENS = os.environ.get("GH_COMMIT_MODS_MAX_TOKENS", "2000")
|
|
||||||
MODS_TIMEOUT = int(os.environ.get("GH_COMMIT_MODS_TIMEOUT", "120"))
|
|
||||||
|
|
||||||
AUTO_CONFIRM = os.environ.get("GH_COMMIT_AUTO", "0") == "1"
|
AUTO_CONFIRM = os.environ.get("GH_COMMIT_AUTO", "0") == "1"
|
||||||
AUTO_PUSH = os.environ.get("GH_COMMIT_PUSH", "0") == "1"
|
AUTO_PUSH = os.environ.get("GH_COMMIT_PUSH", "0") == "1"
|
||||||
@@ -100,10 +96,10 @@ def require_git() -> Path:
|
|||||||
return get_repo_root()
|
return get_repo_root()
|
||||||
|
|
||||||
|
|
||||||
# ── mods CLI client ───────────────────────────────────────────────────────────
|
# ── Crush CLI client ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class ModsError(RuntimeError):
|
class CrushError(RuntimeError):
|
||||||
"""Raised when the `mods` CLI fails to produce output."""
|
"""Raised when `crush run` fails to produce output."""
|
||||||
|
|
||||||
|
|
||||||
def build_filetree(repo_path: Path) -> str:
|
def build_filetree(repo_path: Path) -> str:
|
||||||
@@ -114,44 +110,25 @@ def build_filetree(repo_path: Path) -> str:
|
|||||||
return "\n".join(files)
|
return "\n".join(files)
|
||||||
|
|
||||||
|
|
||||||
def _mods_mcp_servers() -> list[str]:
|
def crush_prompt(text: str, cwd: Path) -> str:
|
||||||
"""Names of MCP servers mods would attach. Disabling them keeps scope
|
"""Send `text` to `crush run` and return its reply."""
|
||||||
generation a plain completion — function-calling models otherwise try to
|
cmd = [*CRUSH_CMD, "run", "--quiet"]
|
||||||
emit the JSON as a (failing) tool call instead of as text."""
|
if CRUSH_MODEL:
|
||||||
result = subprocess.run([*MODS_CMD, "--mcp-list"], capture_output=True, text=True)
|
cmd += ["--model", CRUSH_MODEL]
|
||||||
if result.returncode != 0:
|
|
||||||
return []
|
|
||||||
names = []
|
|
||||||
for line in result.stdout.splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if line:
|
|
||||||
names.append(line.split()[0])
|
|
||||||
return names
|
|
||||||
|
|
||||||
|
|
||||||
def mods_prompt(text: str, role: str, cwd: Path) -> str:
|
|
||||||
"""Pipe `text` into the `mods` CLI under `role` and return its raw reply."""
|
|
||||||
cmd = [*MODS_CMD, "-R", role, "-q", "-r", "--no-limit", "--max-tokens", MODS_MAX_TOKENS]
|
|
||||||
if MODS_API:
|
|
||||||
cmd += ["-a", MODS_API]
|
|
||||||
if MODS_MODEL:
|
|
||||||
cmd += ["-m", MODS_MODEL]
|
|
||||||
for server in _mods_mcp_servers():
|
|
||||||
cmd += ["--mcp-disable", server]
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
cmd, input=text, capture_output=True, text=True,
|
cmd, input=text, capture_output=True, text=True,
|
||||||
cwd=str(cwd), timeout=MODS_TIMEOUT,
|
cwd=str(cwd), timeout=CRUSH_TIMEOUT,
|
||||||
)
|
)
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
raise ModsError(
|
raise CrushError(
|
||||||
f"Could not run the mods CLI ({MODS_CMD[0]!r} not found). "
|
f"Could not run the Crush CLI ({CRUSH_CMD[0]!r} not found). "
|
||||||
"Install mods, or set GH_COMMIT_MODS_CMD to a working command."
|
"Install Crush, or set GH_COMMIT_CRUSH_CMD to a working command."
|
||||||
) from e
|
) from e
|
||||||
except subprocess.TimeoutExpired as e:
|
except subprocess.TimeoutExpired as e:
|
||||||
raise ModsError(f"mods timed out after {MODS_TIMEOUT}s") from e
|
raise CrushError(f"crush run timed out after {CRUSH_TIMEOUT}s") from e
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise ModsError(result.stderr.strip() or f"mods exited with {result.returncode}")
|
raise CrushError(result.stderr.strip() or f"crush run exited with {result.returncode}")
|
||||||
return result.stdout.strip()
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -435,24 +412,53 @@ def filter_diff(diff: str) -> str:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
# ── AI integration (mods CLI) ─────────────────────────────────────────────────
|
# ── AI integration (Crush CLI) ────────────────────────────────────────────────
|
||||||
|
|
||||||
# The `write-commit` mods role carries the Conventional Commits format rules;
|
|
||||||
# this body just supplies the scope to use and the diff to describe.
|
|
||||||
COMMIT_PROMPT = """\
|
COMMIT_PROMPT = """\
|
||||||
|
You are an expert conventional commit message writer.
|
||||||
|
Use one of these commit types: feat, fix, docs, style, refactor, breaking, test, perf, build, ci, chore, init.
|
||||||
|
Determine a precise commit message from the provided diff and scope.
|
||||||
|
The commit message must follow this format: <type>(<scope>): <description>
|
||||||
|
Examples:
|
||||||
|
- fix(core): resolve consensus timeout during high load
|
||||||
|
- feat(did): add WebAuthn biometric authentication support
|
||||||
|
- refactor(hway): extract Redis connection pooling to shared utility
|
||||||
|
- init(browser): setup client package for web API-specific logic
|
||||||
|
- perf(vault): optimize IPFS chunk size for large file uploads
|
||||||
|
- ci(actions): update GitHub Actions pipeline for parallel module testing
|
||||||
|
- docs(dwn): clarify data retention policies in API reference
|
||||||
|
- test(svc): add integration tests for domain verification flow
|
||||||
|
- breaking(ui): rename Button prop 'type' to 'variant'
|
||||||
|
- refactor(sdk): migrate off wallet implementation in favor of @sonr.io/enclave
|
||||||
|
- feat(react): create Enclave stateful hooks
|
||||||
|
Never explain anything. Return only the commit message.
|
||||||
|
|
||||||
{scope_hint}### Git Diff
|
{scope_hint}### Git Diff
|
||||||
{diff}
|
{diff}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The `scope-identifier` mods role carries the output-format rules; these prompt
|
SCOPE_INSTRUCTIONS = """\
|
||||||
# bodies just frame the piped-in file tree.
|
You are a repository scope identifier for Conventional Commits.
|
||||||
SCOPE_PROMPT_NEW = """\
|
You receive a repository file tree, one repo-relative path per line.
|
||||||
|
Map logical project areas to the repo-relative path prefixes (directories or files) they cover.
|
||||||
|
Scope names must read well in Conventional Commits, e.g. core, api, ui, cli, docs, tests, ci, config, scripts, deps.
|
||||||
|
Each scope maps to a JSON array of repo-relative path prefixes.
|
||||||
|
Group related paths together and cover the meaningful source areas.
|
||||||
|
Never create scopes for generated, vendored, or build-output paths.
|
||||||
|
If existing scopes are provided, keep the ones that still apply, drop scopes whose paths no longer exist, and add scopes for new areas.
|
||||||
|
Never explain anything or print your thoughts. Never include Markdown code blocks.
|
||||||
|
Only return a single JSON object of the form {{"scope": ["path", ...], ...}}.
|
||||||
|
"""
|
||||||
|
|
||||||
|
SCOPE_PROMPT_NEW = SCOPE_INSTRUCTIONS + """
|
||||||
|
|
||||||
Repository file tree (one repo-relative path per line):
|
Repository file tree (one repo-relative path per line):
|
||||||
|
|
||||||
{filetree}
|
{filetree}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SCOPE_PROMPT_UPDATE = """\
|
SCOPE_PROMPT_UPDATE = SCOPE_INSTRUCTIONS + """
|
||||||
|
|
||||||
Existing scopes (JSON):
|
Existing scopes (JSON):
|
||||||
{existing}
|
{existing}
|
||||||
|
|
||||||
@@ -502,9 +508,9 @@ def generate_commit_message(diff: str, repo_path: Path, scope: Optional[str] = N
|
|||||||
scope_hint = f"Use this scope: {scope}\n" if scope else ""
|
scope_hint = f"Use this scope: {scope}\n" if scope else ""
|
||||||
prompt = COMMIT_PROMPT.format(scope_hint=scope_hint, diff=filter_diff(diff))
|
prompt = COMMIT_PROMPT.format(scope_hint=scope_hint, diff=filter_diff(diff))
|
||||||
try:
|
try:
|
||||||
message = mods_prompt(prompt, MODS_COMMIT_ROLE, repo_path)
|
message = crush_prompt(prompt, repo_path)
|
||||||
except ModsError as e:
|
except CrushError as e:
|
||||||
console.print(f"[red]mods failed to generate a commit message: {e}[/]")
|
console.print(f"[red]Crush failed to generate a commit message: {e}[/]")
|
||||||
return None
|
return None
|
||||||
# Strip stray code fences if the model added them anyway.
|
# Strip stray code fences if the model added them anyway.
|
||||||
message = re.sub(r"^```[a-zA-Z]*\n|\n```$", "", message.strip()).strip()
|
message = re.sub(r"^```[a-zA-Z]*\n|\n```$", "", message.strip()).strip()
|
||||||
@@ -521,14 +527,14 @@ def generate_scopes(repo_path: Path, existing: Optional[dict[str, list[str]]] =
|
|||||||
else:
|
else:
|
||||||
prompt = SCOPE_PROMPT_NEW.format(filetree=filetree)
|
prompt = SCOPE_PROMPT_NEW.format(filetree=filetree)
|
||||||
try:
|
try:
|
||||||
with console.status("[magenta]Analyzing repository with mods...[/]"):
|
with console.status("[magenta]Analyzing repository with Crush...[/]"):
|
||||||
output = mods_prompt(prompt, MODS_SCOPE_ROLE, repo_path)
|
output = crush_prompt(prompt, repo_path)
|
||||||
except ModsError as e:
|
except CrushError as e:
|
||||||
console.print(f"[red]mods failed: {e}[/]")
|
console.print(f"[red]Crush failed: {e}[/]")
|
||||||
return None
|
return None
|
||||||
scopes = parse_scopes_response(output)
|
scopes = parse_scopes_response(output)
|
||||||
if not scopes:
|
if not scopes:
|
||||||
console.print("[red]Could not parse scopes from mods' response[/]")
|
console.print("[red]Could not parse scopes from Crush's response[/]")
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
console.print(f"[dim]{output}[/]")
|
console.print(f"[dim]{output}[/]")
|
||||||
return scopes
|
return scopes
|
||||||
@@ -626,7 +632,7 @@ def maybe_auto_refresh_scopes(repo_path: Path, repo_name: str):
|
|||||||
if current == stored:
|
if current == stored:
|
||||||
return
|
return
|
||||||
|
|
||||||
console.print("[yellow]↻ .gitignore changed — regenerating scopes with Claude...[/]")
|
console.print("[yellow]↻ .gitignore changed — regenerating scopes with Crush...[/]")
|
||||||
existing = get_repo_scopes(str(repo_path))
|
existing = get_repo_scopes(str(repo_path))
|
||||||
scopes = generate_scopes(repo_path, existing)
|
scopes = generate_scopes(repo_path, existing)
|
||||||
if not scopes:
|
if not scopes:
|
||||||
@@ -645,7 +651,7 @@ def cmd_version():
|
|||||||
|
|
||||||
|
|
||||||
def cmd_help():
|
def cmd_help():
|
||||||
console.print(f"[magenta bold]gh commit[/] [dim]v{VERSION}[/] — AI-powered scoped git commits (mods)\n")
|
console.print(f"[magenta bold]gh commit[/] [dim]v{VERSION}[/] — AI-powered scoped git commits (Crush)\n")
|
||||||
console.print("[cyan]Usage:[/]")
|
console.print("[cyan]Usage:[/]")
|
||||||
console.print(" gh commit Commit changes grouped by scope")
|
console.print(" gh commit Commit changes grouped by scope")
|
||||||
console.print(" gh commit --auto Auto-confirm all prompts")
|
console.print(" gh commit --auto Auto-confirm all prompts")
|
||||||
@@ -665,11 +671,9 @@ def cmd_help():
|
|||||||
console.print(" GH_COMMIT_AUTO=1 Skip all confirmation prompts")
|
console.print(" GH_COMMIT_AUTO=1 Skip all confirmation prompts")
|
||||||
console.print(" GH_COMMIT_PUSH=1 Auto-push after commits")
|
console.print(" GH_COMMIT_PUSH=1 Auto-push after commits")
|
||||||
console.print(" GH_COMMIT_NO_AUTO_REFRESH=1 Don't auto-regenerate scopes on .gitignore change")
|
console.print(" GH_COMMIT_NO_AUTO_REFRESH=1 Don't auto-regenerate scopes on .gitignore change")
|
||||||
console.print(" GH_COMMIT_MODS_CMD=... Override the mods command")
|
console.print(" GH_COMMIT_CRUSH_CMD=... Override the Crush command")
|
||||||
console.print(" GH_COMMIT_MODS_SCOPE_ROLE= mods role for scopes (default scope-identifier)")
|
console.print(" GH_COMMIT_CRUSH_MODEL=... Override the Crush model (default: openrouter/qwen/qwen3.6-27b)")
|
||||||
console.print(" GH_COMMIT_MODS_COMMIT_ROLE= mods role for commit messages (default write-commit)")
|
console.print(" GH_COMMIT_CRUSH_TIMEOUT=... Per-prompt timeout in seconds (default: 120)")
|
||||||
console.print(" GH_COMMIT_MODS_API=... Override the mods API (default: openrouter)")
|
|
||||||
console.print(" GH_COMMIT_MODS_MODEL=... Override the mods model (default: qwen/qwen3.6-27b)")
|
|
||||||
console.print(" GH_COMMIT_DEBUG=1 Show parse diagnostics\n")
|
console.print(" GH_COMMIT_DEBUG=1 Show parse diagnostics\n")
|
||||||
console.print("[cyan]Scopes auto-refresh whenever .gitignore changes.[/]")
|
console.print("[cyan]Scopes auto-refresh whenever .gitignore changes.[/]")
|
||||||
console.print("[cyan]Legacy .github/Repo.toml or scopes.json are auto-migrated on first run.[/]")
|
console.print("[cyan]Legacy .github/Repo.toml or scopes.json are auto-migrated on first run.[/]")
|
||||||
@@ -830,7 +834,7 @@ def cmd_commit():
|
|||||||
if not repo_has_scopes(str(repo_path)):
|
if not repo_has_scopes(str(repo_path)):
|
||||||
console.print("\n[yellow bold]⚠ No scopes configured for this repository[/]\n")
|
console.print("\n[yellow bold]⚠ No scopes configured for this repository[/]\n")
|
||||||
console.print("[cyan]gh-commit organizes commits by project areas (scopes).[/]\n")
|
console.print("[cyan]gh-commit organizes commits by project areas (scopes).[/]\n")
|
||||||
if confirm("Generate scopes now using Claude?"):
|
if confirm("Generate scopes now using Crush?"):
|
||||||
if cmd_init() != 0:
|
if cmd_init() != 0:
|
||||||
return 1
|
return 1
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user