pushpilot 1.1.0__py3-none-any.whl
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.
- autopush/__init__.py +6 -0
- autopush/ai_commit.py +483 -0
- autopush/cli.py +316 -0
- autopush/commit_gen.py +144 -0
- autopush/detector.py +342 -0
- autopush/git_ops.py +256 -0
- autopush/watcher.py +159 -0
- pushpilot-1.1.0.dist-info/METADATA +155 -0
- pushpilot-1.1.0.dist-info/RECORD +13 -0
- pushpilot-1.1.0.dist-info/WHEEL +5 -0
- pushpilot-1.1.0.dist-info/entry_points.txt +6 -0
- pushpilot-1.1.0.dist-info/licenses/LICENSE +23 -0
- pushpilot-1.1.0.dist-info/top_level.txt +1 -0
autopush/cli.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Autopush Command-Line Interface.
|
|
3
|
+
Professional multi-language & multi-framework Git automation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
# Ensure UTF-8 console output on Windows
|
|
11
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
12
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
13
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
14
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
15
|
+
|
|
16
|
+
from autopush import __version__
|
|
17
|
+
from autopush.commit_gen import generate_commit_message
|
|
18
|
+
from autopush.detector import StackDetector
|
|
19
|
+
from autopush.git_ops import (
|
|
20
|
+
GitError,
|
|
21
|
+
commit,
|
|
22
|
+
ensure_gitignore,
|
|
23
|
+
ensure_repo_ready,
|
|
24
|
+
get_current_branch,
|
|
25
|
+
get_git_diff_summary,
|
|
26
|
+
get_git_status_changes,
|
|
27
|
+
get_remotes,
|
|
28
|
+
has_unpushed_commits,
|
|
29
|
+
is_git_repo,
|
|
30
|
+
push,
|
|
31
|
+
setup_remote_interactive,
|
|
32
|
+
stage_all,
|
|
33
|
+
)
|
|
34
|
+
from autopush.watcher import FeatureWatcher
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def banner():
|
|
38
|
+
print(
|
|
39
|
+
f"""
|
|
40
|
+
╔═════════════════════════════════════════════════════════════╗
|
|
41
|
+
║ PUSHPILOT v{__version__} ║
|
|
42
|
+
║ Intelligent Multi-Framework Git & GitHub Autopilot ║
|
|
43
|
+
╚═════════════════════════════════════════════════════════════╝
|
|
44
|
+
"""
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def run_single_sync(
|
|
49
|
+
root: Path,
|
|
50
|
+
repo_url: str | None = None,
|
|
51
|
+
message: str | None = None,
|
|
52
|
+
branch: str | None = None,
|
|
53
|
+
remote: str = "origin",
|
|
54
|
+
dry_run: bool = False,
|
|
55
|
+
interactive: bool = True,
|
|
56
|
+
allow_ai: bool = True,
|
|
57
|
+
) -> bool:
|
|
58
|
+
"""Performs a full automated check, stage, commit, and push cycle."""
|
|
59
|
+
# Disable interactive prompts if dry_run or stdin is not a terminal
|
|
60
|
+
if dry_run or not sys.stdin.isatty():
|
|
61
|
+
interactive = False
|
|
62
|
+
|
|
63
|
+
# 1. Detect Stack
|
|
64
|
+
stack = StackDetector.detect(root)
|
|
65
|
+
print(f"[STACK] Detected: {stack.language} | Framework: {stack.framework or 'None'} | Ecosystem: {stack.ecosystem}")
|
|
66
|
+
|
|
67
|
+
# 2. Ensure Git & Remote Setup
|
|
68
|
+
ready, remote_was_just_linked = ensure_repo_ready(root, repo_url=repo_url, interactive=interactive)
|
|
69
|
+
|
|
70
|
+
# 3. Ensure .gitignore
|
|
71
|
+
ensure_gitignore(root, stack, interactive=interactive)
|
|
72
|
+
|
|
73
|
+
# Active branch and remotes
|
|
74
|
+
active_branch = branch or get_current_branch(root)
|
|
75
|
+
remotes = get_remotes(root)
|
|
76
|
+
|
|
77
|
+
print(f"[BRANCH] Active Branch : {active_branch}")
|
|
78
|
+
if remote in remotes:
|
|
79
|
+
print(f"[REMOTE] Target Remote : {remote} ({remotes[remote]})")
|
|
80
|
+
else:
|
|
81
|
+
print(f"[REMOTE] Warning: Remote '{remote}' is not configured.")
|
|
82
|
+
|
|
83
|
+
# 4. Check Changes
|
|
84
|
+
changes = get_git_status_changes(root)
|
|
85
|
+
if not changes:
|
|
86
|
+
remotes = get_remotes(root)
|
|
87
|
+
if remote in remotes and (remote_was_just_linked or has_unpushed_commits(root, remote, active_branch)):
|
|
88
|
+
print("[INFO] Working tree clean, but unpushed commits exist. Pushing to GitHub...")
|
|
89
|
+
if not dry_run:
|
|
90
|
+
try:
|
|
91
|
+
push(root, remote, active_branch)
|
|
92
|
+
print(f"\n[SUCCESS] Pushed all commits to GitHub ({remote}/{active_branch}) successfully!")
|
|
93
|
+
except GitError as e:
|
|
94
|
+
print(f"\n[ERROR] Push failed: {e}")
|
|
95
|
+
return False
|
|
96
|
+
else:
|
|
97
|
+
print(f"[DRY-RUN] Would push to {remote}/{active_branch}")
|
|
98
|
+
return True
|
|
99
|
+
else:
|
|
100
|
+
print("[INFO] Working tree clean and up to date with remote. Nothing to push.")
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
print(f"[CHANGES] Detected {len(changes)} modified/new file(s):")
|
|
104
|
+
for status, file_path in changes[:8]:
|
|
105
|
+
print(f" [{status or '?'}] {file_path}")
|
|
106
|
+
if len(changes) > 8:
|
|
107
|
+
print(f" ... and {len(changes) - 8} more file(s)")
|
|
108
|
+
|
|
109
|
+
# 5. Generate Commit Message
|
|
110
|
+
diff_summary = get_git_diff_summary(root)
|
|
111
|
+
commit_msg, ai_source = generate_commit_message(
|
|
112
|
+
changes,
|
|
113
|
+
stack,
|
|
114
|
+
custom_msg=message,
|
|
115
|
+
root=root,
|
|
116
|
+
diff_summary=diff_summary,
|
|
117
|
+
allow_ai=allow_ai,
|
|
118
|
+
)
|
|
119
|
+
if ai_source:
|
|
120
|
+
print(f"[AI] Detected coding with AI: {ai_source}")
|
|
121
|
+
print(f"[COMMIT] AI-Generated Message: \"{commit_msg}\"")
|
|
122
|
+
else:
|
|
123
|
+
print(f"[COMMIT] Generated Message: \"{commit_msg}\"")
|
|
124
|
+
|
|
125
|
+
if dry_run:
|
|
126
|
+
print(f"[DRY-RUN] Would stage all files ('git add -A')")
|
|
127
|
+
print(f"[DRY-RUN] Would commit with message: \"{commit_msg}\"")
|
|
128
|
+
if remote in remotes:
|
|
129
|
+
print(f"[DRY-RUN] Would push to {remote}/{active_branch}")
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
# 6. Stage & Commit
|
|
133
|
+
print("[STAGING] Staging all files...")
|
|
134
|
+
stage_all(root)
|
|
135
|
+
|
|
136
|
+
print("[COMMITTING] Committing changes...")
|
|
137
|
+
commit(root, commit_msg)
|
|
138
|
+
print("[SUCCESS] Changes committed successfully.")
|
|
139
|
+
|
|
140
|
+
# 7. Push to Remote
|
|
141
|
+
remotes = get_remotes(root)
|
|
142
|
+
if remote not in remotes:
|
|
143
|
+
print(f"\n[NOTICE] Remote '{remote}' not linked yet.")
|
|
144
|
+
if interactive:
|
|
145
|
+
new_remote = setup_remote_interactive(root)
|
|
146
|
+
if not new_remote:
|
|
147
|
+
print("[INFO] Commit saved locally. Link a remote when you are ready to push.")
|
|
148
|
+
return True
|
|
149
|
+
else:
|
|
150
|
+
print(f"[WARNING] Remote '{remote}' not configured. Run 'auto-push <repo-url>' to link a GitHub repo.")
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
print(f"[PUSHING] Pushing to GitHub ({remote}/{active_branch})...")
|
|
154
|
+
try:
|
|
155
|
+
push(root, remote, active_branch)
|
|
156
|
+
print(f"\n[SUCCESS] Feature pushed to GitHub ({remote}/{active_branch}) successfully!")
|
|
157
|
+
return True
|
|
158
|
+
except GitError as e:
|
|
159
|
+
print(f"\n[ERROR] Push failed: {e}")
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def main():
|
|
164
|
+
parser = argparse.ArgumentParser(
|
|
165
|
+
prog="pushpilot",
|
|
166
|
+
description="PushPilot - Intelligent Multi-Framework Git & GitHub Autopilot.",
|
|
167
|
+
)
|
|
168
|
+
parser.add_argument(
|
|
169
|
+
"repo_url",
|
|
170
|
+
nargs="?",
|
|
171
|
+
default=None,
|
|
172
|
+
help="Optional GitHub repository URL (e.g. https://github.com/user/repo.git). Automatically initializes git and links remote.",
|
|
173
|
+
)
|
|
174
|
+
parser.add_argument(
|
|
175
|
+
"-m", "--message",
|
|
176
|
+
type=str,
|
|
177
|
+
default=None,
|
|
178
|
+
help="Custom commit message (if omitted, conventional semantic message is generated).",
|
|
179
|
+
)
|
|
180
|
+
parser.add_argument(
|
|
181
|
+
"-w", "--watch",
|
|
182
|
+
action="store_true",
|
|
183
|
+
help="Enable Feature-Watcher mode: monitors files and auto-pushes when features are completed.",
|
|
184
|
+
)
|
|
185
|
+
parser.add_argument(
|
|
186
|
+
"--cooldown",
|
|
187
|
+
type=float,
|
|
188
|
+
default=8.0,
|
|
189
|
+
help="Feature completion settlement window in seconds for watch mode (default: 8.0s).",
|
|
190
|
+
)
|
|
191
|
+
parser.add_argument(
|
|
192
|
+
"-b", "--branch",
|
|
193
|
+
type=str,
|
|
194
|
+
default=None,
|
|
195
|
+
help="Target git branch (default: active branch).",
|
|
196
|
+
)
|
|
197
|
+
parser.add_argument(
|
|
198
|
+
"-r", "--remote",
|
|
199
|
+
type=str,
|
|
200
|
+
default="origin",
|
|
201
|
+
help="Git remote name (default: 'origin').",
|
|
202
|
+
)
|
|
203
|
+
parser.add_argument(
|
|
204
|
+
"--setup",
|
|
205
|
+
action="store_true",
|
|
206
|
+
help="Interactive GitHub repository setup / change remote URL.",
|
|
207
|
+
)
|
|
208
|
+
parser.add_argument(
|
|
209
|
+
"--detect",
|
|
210
|
+
action="store_true",
|
|
211
|
+
help="Inspect detected language, framework, ecosystem, and build tools for this project.",
|
|
212
|
+
)
|
|
213
|
+
parser.add_argument(
|
|
214
|
+
"--init-gitignore",
|
|
215
|
+
action="store_true",
|
|
216
|
+
help="Generate or reset a framework-tailored .gitignore file.",
|
|
217
|
+
)
|
|
218
|
+
parser.add_argument(
|
|
219
|
+
"--dry-run",
|
|
220
|
+
action="store_true",
|
|
221
|
+
help="Simulate actions without staging, committing, or pushing.",
|
|
222
|
+
)
|
|
223
|
+
parser.add_argument(
|
|
224
|
+
"--no-ai",
|
|
225
|
+
action="store_true",
|
|
226
|
+
help="Disable AI commit message generation and force default rule-based message.",
|
|
227
|
+
)
|
|
228
|
+
parser.add_argument(
|
|
229
|
+
"--ai",
|
|
230
|
+
action="store_true",
|
|
231
|
+
help="Explicitly enable AI commit message generation (auto-detected by default).",
|
|
232
|
+
)
|
|
233
|
+
parser.add_argument(
|
|
234
|
+
"-v", "--version",
|
|
235
|
+
action="version",
|
|
236
|
+
version=f"PushPilot v{__version__}",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
args = parser.parse_args()
|
|
240
|
+
root = Path.cwd()
|
|
241
|
+
allow_ai = not args.no_ai
|
|
242
|
+
|
|
243
|
+
banner()
|
|
244
|
+
|
|
245
|
+
# --detect command
|
|
246
|
+
if args.detect:
|
|
247
|
+
stack = StackDetector.detect(root)
|
|
248
|
+
print("=" * 50)
|
|
249
|
+
print(" PROJECT STACK INSPECTION")
|
|
250
|
+
print("=" * 50)
|
|
251
|
+
print(f"Language : {stack.language}")
|
|
252
|
+
print(f"Framework : {stack.framework or 'None / Standard'}")
|
|
253
|
+
print(f"Ecosystem : {stack.ecosystem}")
|
|
254
|
+
print(f"Build Tool : {stack.build_tool or 'None detected'}")
|
|
255
|
+
print(f"Git Repo : {'Yes' if is_git_repo(root) else 'No (run auto-push to initialize)'}")
|
|
256
|
+
remotes = get_remotes(root) if is_git_repo(root) else {}
|
|
257
|
+
print(f"Remotes : {remotes or 'None'}")
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
# --init-gitignore command
|
|
261
|
+
if args.init_gitignore:
|
|
262
|
+
stack = StackDetector.detect(root)
|
|
263
|
+
ensure_gitignore(root, stack, interactive=False)
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
# --setup command
|
|
267
|
+
if args.setup:
|
|
268
|
+
ensure_repo_ready(root, repo_url=args.repo_url, interactive=False)
|
|
269
|
+
setup_remote_interactive(root)
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
# --watch mode
|
|
273
|
+
if args.watch:
|
|
274
|
+
stack = StackDetector.detect(root)
|
|
275
|
+
ensure_repo_ready(root, repo_url=args.repo_url, interactive=True)
|
|
276
|
+
ensure_gitignore(root, stack, interactive=True)
|
|
277
|
+
|
|
278
|
+
def on_feature_complete(changes):
|
|
279
|
+
run_single_sync(
|
|
280
|
+
root=root,
|
|
281
|
+
repo_url=args.repo_url,
|
|
282
|
+
message=args.message,
|
|
283
|
+
branch=args.branch,
|
|
284
|
+
remote=args.remote,
|
|
285
|
+
dry_run=args.dry_run,
|
|
286
|
+
interactive=False,
|
|
287
|
+
allow_ai=allow_ai,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
watcher = FeatureWatcher(
|
|
291
|
+
root=root,
|
|
292
|
+
stack=stack,
|
|
293
|
+
on_feature_complete=on_feature_complete,
|
|
294
|
+
settle_window_sec=args.cooldown,
|
|
295
|
+
)
|
|
296
|
+
watcher.start()
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
# Default: One-command sync
|
|
300
|
+
success = run_single_sync(
|
|
301
|
+
root=root,
|
|
302
|
+
repo_url=args.repo_url,
|
|
303
|
+
message=args.message,
|
|
304
|
+
branch=args.branch,
|
|
305
|
+
remote=args.remote,
|
|
306
|
+
dry_run=args.dry_run,
|
|
307
|
+
interactive=True,
|
|
308
|
+
allow_ai=allow_ai,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if not success:
|
|
312
|
+
sys.exit(1)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
if __name__ == "__main__":
|
|
316
|
+
main()
|
autopush/commit_gen.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Conventional Commit Message Generator.
|
|
3
|
+
Generates human-readable, semantic, and professional commit messages:
|
|
4
|
+
1. If the developer is coding with AI (Google Antigravity, Cursor, Aider,
|
|
5
|
+
Claude Code, Cline, or configured Gemini/OpenAI/Anthropic/Groq/Ollama APIs),
|
|
6
|
+
it leverages the internal AI context to produce an ultra-accurate commit message.
|
|
7
|
+
2. If the developer is not coding with AI, it automatically falls back to the
|
|
8
|
+
default rule-based heuristic generator.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from autopush.detector import ProjectStack, classify_file_role
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def generate_default_commit_message(
|
|
18
|
+
changes: list[tuple[str, str]],
|
|
19
|
+
stack: ProjectStack,
|
|
20
|
+
) -> str:
|
|
21
|
+
"""Default rule-based conventional commit generator (used when not coding with AI)."""
|
|
22
|
+
if not changes:
|
|
23
|
+
return f"chore: sync project updates [{stack.language}]"
|
|
24
|
+
|
|
25
|
+
# Analyze change classifications
|
|
26
|
+
roles: dict[str, list[str]] = {}
|
|
27
|
+
actions = {"add": 0, "modify": 0, "delete": 0}
|
|
28
|
+
|
|
29
|
+
for status, file_path in changes:
|
|
30
|
+
if "?" in status or "A" in status:
|
|
31
|
+
actions["add"] += 1
|
|
32
|
+
elif "D" in status:
|
|
33
|
+
actions["delete"] += 1
|
|
34
|
+
else:
|
|
35
|
+
actions["modify"] += 1
|
|
36
|
+
|
|
37
|
+
role, comp = classify_file_role(file_path)
|
|
38
|
+
roles.setdefault(role, []).append(Path(file_path).name)
|
|
39
|
+
|
|
40
|
+
# Determine primary role
|
|
41
|
+
# Priority: auth > api > ai > model > ui > service > test > docs > config > core
|
|
42
|
+
priority_order = ["auth", "api", "ai", "model", "ui", "service", "test", "docs", "config", "core"]
|
|
43
|
+
primary_role = "core"
|
|
44
|
+
for r in priority_order:
|
|
45
|
+
if r in roles:
|
|
46
|
+
primary_role = r
|
|
47
|
+
break
|
|
48
|
+
|
|
49
|
+
# Determine Conventional Commit Prefix
|
|
50
|
+
if primary_role == "docs":
|
|
51
|
+
prefix = "docs"
|
|
52
|
+
elif primary_role == "test":
|
|
53
|
+
prefix = "test"
|
|
54
|
+
elif primary_role == "config":
|
|
55
|
+
prefix = "chore(deps)" if any("package" in f or "req" in f or "cargo" in f for f in roles[primary_role]) else "chore(config)"
|
|
56
|
+
elif actions["add"] > actions["modify"]:
|
|
57
|
+
prefix = f"feat({primary_role})"
|
|
58
|
+
elif actions["delete"] > (actions["add"] + actions["modify"]):
|
|
59
|
+
prefix = f"refactor({primary_role})"
|
|
60
|
+
else:
|
|
61
|
+
prefix = f"refactor({primary_role})" if actions["modify"] > 0 else f"feat({primary_role})"
|
|
62
|
+
|
|
63
|
+
# Construct descriptive subject
|
|
64
|
+
primary_files = roles.get(primary_role, [Path(changes[0][1]).name])
|
|
65
|
+
file_sample = primary_files[0]
|
|
66
|
+
stem_name = Path(file_sample).stem.replace("_", " ").replace("-", " ").title()
|
|
67
|
+
|
|
68
|
+
if primary_role == "docs":
|
|
69
|
+
subject = "update project documentation and guides"
|
|
70
|
+
elif primary_role == "config":
|
|
71
|
+
subject = "update configuration and dependencies"
|
|
72
|
+
elif primary_role == "test":
|
|
73
|
+
subject = f"add/update test suite for {stem_name}"
|
|
74
|
+
elif primary_role == "auth":
|
|
75
|
+
subject = f"implement authentication workflow ({stem_name})"
|
|
76
|
+
elif primary_role == "api":
|
|
77
|
+
subject = f"update API endpoints and handlers ({stem_name})"
|
|
78
|
+
elif primary_role == "model":
|
|
79
|
+
subject = f"update database schemas and data models ({stem_name})"
|
|
80
|
+
elif primary_role == "ui":
|
|
81
|
+
subject = f"update UI components and layouts ({stem_name})"
|
|
82
|
+
elif primary_role == "style":
|
|
83
|
+
subject = f"improve styling and visual presentation ({stem_name})"
|
|
84
|
+
elif primary_role == "ai":
|
|
85
|
+
subject = f"integrate AI-assisted features and enhancements ({stem_name})"
|
|
86
|
+
else:
|
|
87
|
+
if actions["add"] > 0 and actions["modify"] == 0:
|
|
88
|
+
subject = f"add new {stem_name} module"
|
|
89
|
+
elif actions["delete"] > 0 and actions["add"] == 0:
|
|
90
|
+
subject = f"clean up and remove {stem_name}"
|
|
91
|
+
else:
|
|
92
|
+
subject = f"update {stem_name} and related features"
|
|
93
|
+
|
|
94
|
+
# Context tag if framework is detected
|
|
95
|
+
fw_tag = f" in {stack.framework}" if stack.framework and stack.framework != "Generic Project" else ""
|
|
96
|
+
|
|
97
|
+
return f"{prefix}: {subject}{fw_tag}"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def generate_commit_message(
|
|
101
|
+
changes: list[tuple[str, str]],
|
|
102
|
+
stack: ProjectStack,
|
|
103
|
+
custom_msg: Optional[str] = None,
|
|
104
|
+
root: Optional[Path] = None,
|
|
105
|
+
diff_summary: Optional[str] = None,
|
|
106
|
+
allow_ai: bool = True,
|
|
107
|
+
) -> tuple[str, Optional[str]]:
|
|
108
|
+
"""
|
|
109
|
+
Generates a professional conventional commit message.
|
|
110
|
+
|
|
111
|
+
Workflow:
|
|
112
|
+
1. If user provided a custom commit message (-m / --message), use it directly.
|
|
113
|
+
2. If allow_ai is True, checks if the developer is coding with AI (Antigravity IDE,
|
|
114
|
+
Cursor, Aider, Claude Code, Cline, or configured Gemini/OpenAI/Anthropic/Groq/Ollama APIs).
|
|
115
|
+
If AI is detected, generates an accurate message using the internal AI context.
|
|
116
|
+
3. If the developer is NOT coding with AI, seamlessly uses the default heuristic generator.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
(commit_message, ai_source_description_or_none)
|
|
120
|
+
"""
|
|
121
|
+
if custom_msg and custom_msg.strip():
|
|
122
|
+
return custom_msg.strip(), None
|
|
123
|
+
|
|
124
|
+
if not changes:
|
|
125
|
+
return f"chore: sync project updates [{stack.language}]", None
|
|
126
|
+
|
|
127
|
+
# Check internal AI if enabled
|
|
128
|
+
if allow_ai:
|
|
129
|
+
try:
|
|
130
|
+
from autopush.ai_commit import generate_ai_commit_message
|
|
131
|
+
ai_msg, ai_source = generate_ai_commit_message(
|
|
132
|
+
changes=changes,
|
|
133
|
+
stack=stack,
|
|
134
|
+
diff_summary=diff_summary or "",
|
|
135
|
+
root=root or Path.cwd(),
|
|
136
|
+
)
|
|
137
|
+
if ai_msg:
|
|
138
|
+
return ai_msg, ai_source
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
# Seamless fallback to default rule-based commit generator
|
|
143
|
+
default_msg = generate_default_commit_message(changes, stack)
|
|
144
|
+
return default_msg, None
|