ace-git-copilot 0.3.0__tar.gz → 0.3.2__tar.gz
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.
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/PKG-INFO +30 -1
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/README.md +29 -0
- ace_git_copilot-0.3.2/ace/__init__.py +1 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/changelog_generator.py +18 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/commit_generator.py +7 -2
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/llm_factory.py +7 -6
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/cli.py +34 -13
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ui/dashboard.py +12 -10
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ui/display.py +61 -2
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ui/prompts.py +3 -2
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/pyproject.toml +1 -1
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/e2e/conftest.py +63 -34
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/e2e/test_tier1_features.py +18 -6
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/e2e/test_tier2_boundaries.py +12 -7
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/e2e/test_tier3_combinations.py +9 -3
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/e2e/test_tier4_workloads.py +11 -5
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_help.py +2 -2
- ace_git_copilot-0.3.0/ace/__init__.py +0 -1
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/AGENTS.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/BRIEFING.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/ORIGINAL_REQUEST.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/e2e_testing_track/BRIEFING.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/e2e_testing_track/ORIGINAL_REQUEST.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/e2e_testing_track/SCOPE.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/e2e_testing_track/progress.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/BRIEFING.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/ORIGINAL_REQUEST.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/emojis_list.txt +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/find_unused_modules.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/handoff.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/measure_lazy_startup.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/measure_startup.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/profile_imports.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/progress.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/run_importtime.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/search_banner.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/search_emojis.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/search_git_usages.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/search_usages.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/test_import_profiler.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/explorer_init/test_mocked_sys.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/handoff.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/implementation_track/BRIEFING.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/implementation_track/ORIGINAL_REQUEST.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/implementation_track/explorer_initial_report.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/implementation_track/progress.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/orchestrator/.gitkeep +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/orchestrator/BRIEFING.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/orchestrator/ORIGINAL_REQUEST.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/orchestrator/PROJECT.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/orchestrator/progress.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/teamwork_preview_explorer_e2e_explore/BRIEFING.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/teamwork_preview_explorer_e2e_explore/ORIGINAL_REQUEST.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/teamwork_preview_explorer_e2e_explore/handoff.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/teamwork_preview_explorer_e2e_explore/progress.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/worker_e2e_testing/BRIEFING.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/worker_e2e_testing/ORIGINAL_REQUEST.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/worker_e2e_testing/progress.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/worker_m1_startup/BRIEFING.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/worker_m1_startup/ORIGINAL_REQUEST.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.agents/worker_m1_startup/progress.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.env.example +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.github/workflows/tests.yml +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/.gitignore +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/CODE_OF_CONDUCT.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/CONTRIBUTING.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/LICENSE +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/PROJECT.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/SECURITY.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/SUPPORT.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/TEST_INFRA.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/TEST_READY.md +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/__main__.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/code_reviewer.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/conflict_resolver.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/gitignore_generator.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/history_analyzer.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/intent_parser.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/pr_drafter.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/changelog.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/commit.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/conflict.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/doctor.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/explain.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/ignore.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/intent.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/pr.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/rebase.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/review.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/search.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/prompts/undo.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ai/rebase_helper.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/core/config.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/core/context.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/core/diagnostics.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/core/git_ops.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/core/hooks.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/core/safety.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ui/banner.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/ui/themes.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/utils/conflict_parser.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/utils/diff_parser.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/ace/utils/json_utils.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/importtime.txt +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/importtime_optimized.txt +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/conftest.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_changelog_generator.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_code_reviewer.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_conflict_resolver.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_diagnostics.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_diff_trimmer.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_git_ops.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_history_analyzer.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_hooks.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_ignore.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_intent_parser.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_llm_factory.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_pr_drafter.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_rebase_helper.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_safety.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_search.py +0 -0
- {ace_git_copilot-0.3.0 → ace_git_copilot-0.3.2}/tests/test_undo.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ace-git-copilot
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: AI-powered Git copilot — talk to Git in plain English
|
|
5
5
|
Project-URL: Homepage, https://github.com/jachinsamuel/Ace
|
|
6
6
|
Project-URL: Documentation, https://github.com/jachinsamuel/Ace#readme
|
|
@@ -193,3 +193,32 @@ The dashboard features:
|
|
|
193
193
|
## License
|
|
194
194
|
|
|
195
195
|
Distributed under the MIT License. See [LICENSE](LICENSE) for more details.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Changelog
|
|
200
|
+
|
|
201
|
+
### v0.3.1 — Patch (2026-06-30)
|
|
202
|
+
* Fixed invisible key labels (`[c]`, `[r]`, etc.) in the dashboard and search menus caused by Rich markup tag conflicts.
|
|
203
|
+
* Fixed `[Y/n]` confirmation prompt rendering invisibly.
|
|
204
|
+
|
|
205
|
+
### v0.3.0 — UI Polish (2026-06-30)
|
|
206
|
+
* Rewrote all UI output modules (`display.py`, `prompts.py`, `dashboard.py`, `themes.py`) for a polished, professional look.
|
|
207
|
+
* Removed all unprofessional emojis; replaced with clean ASCII status symbols (`>>`, `**`, `!!`, `EE`).
|
|
208
|
+
* Dashboard panels now use consistent `ROUNDED` borders, colour-coded file lists (`+` staged, `~` unstaged, `?` untracked), and a styled commit history table.
|
|
209
|
+
* Execution plan table uses `SIMPLE_HEAD` style with colour-highlighted `git` and `ace` command prefixes.
|
|
210
|
+
* Commit message panel shows character count badge coloured green/amber/red relative to the 72-char limit.
|
|
211
|
+
* AI code review score rendered as a styled badge based on score range.
|
|
212
|
+
* Added `[tool.ruff]` configuration to exclude `.agents/` scratch folder from lint checks.
|
|
213
|
+
|
|
214
|
+
### v0.2.9 — Metadata & Build Fix (2026-06-30)
|
|
215
|
+
* Fixed broken `pyproject.toml` TOML syntax (invalid inline `urls` table).
|
|
216
|
+
* Added full `authors` and `[project.urls]` metadata visible on PyPI.
|
|
217
|
+
|
|
218
|
+
### v0.2.6 — Startup Optimisation (2026-06-21)
|
|
219
|
+
* Implemented lazy-loading for all heavy LangChain imports — CLI startup under 200 ms.
|
|
220
|
+
* Added `ace switch` command for switching between sibling repositories from the dashboard.
|
|
221
|
+
|
|
222
|
+
### v0.2.3 — Feature Expansion (2026-06-09)
|
|
223
|
+
* Added `ace doctor`, `ace hook`, and `ace squash` commands.
|
|
224
|
+
* Comprehensive E2E test suite added under `tests/e2e/`.
|
|
@@ -160,3 +160,32 @@ The dashboard features:
|
|
|
160
160
|
## License
|
|
161
161
|
|
|
162
162
|
Distributed under the MIT License. See [LICENSE](LICENSE) for more details.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Changelog
|
|
167
|
+
|
|
168
|
+
### v0.3.1 — Patch (2026-06-30)
|
|
169
|
+
* Fixed invisible key labels (`[c]`, `[r]`, etc.) in the dashboard and search menus caused by Rich markup tag conflicts.
|
|
170
|
+
* Fixed `[Y/n]` confirmation prompt rendering invisibly.
|
|
171
|
+
|
|
172
|
+
### v0.3.0 — UI Polish (2026-06-30)
|
|
173
|
+
* Rewrote all UI output modules (`display.py`, `prompts.py`, `dashboard.py`, `themes.py`) for a polished, professional look.
|
|
174
|
+
* Removed all unprofessional emojis; replaced with clean ASCII status symbols (`>>`, `**`, `!!`, `EE`).
|
|
175
|
+
* Dashboard panels now use consistent `ROUNDED` borders, colour-coded file lists (`+` staged, `~` unstaged, `?` untracked), and a styled commit history table.
|
|
176
|
+
* Execution plan table uses `SIMPLE_HEAD` style with colour-highlighted `git` and `ace` command prefixes.
|
|
177
|
+
* Commit message panel shows character count badge coloured green/amber/red relative to the 72-char limit.
|
|
178
|
+
* AI code review score rendered as a styled badge based on score range.
|
|
179
|
+
* Added `[tool.ruff]` configuration to exclude `.agents/` scratch folder from lint checks.
|
|
180
|
+
|
|
181
|
+
### v0.2.9 — Metadata & Build Fix (2026-06-30)
|
|
182
|
+
* Fixed broken `pyproject.toml` TOML syntax (invalid inline `urls` table).
|
|
183
|
+
* Added full `authors` and `[project.urls]` metadata visible on PyPI.
|
|
184
|
+
|
|
185
|
+
### v0.2.6 — Startup Optimisation (2026-06-21)
|
|
186
|
+
* Implemented lazy-loading for all heavy LangChain imports — CLI startup under 200 ms.
|
|
187
|
+
* Added `ace switch` command for switching between sibling repositories from the dashboard.
|
|
188
|
+
|
|
189
|
+
### v0.2.3 — Feature Expansion (2026-06-09)
|
|
190
|
+
* Added `ace doctor`, `ace hook`, and `ace squash` commands.
|
|
191
|
+
* Comprehensive E2E test suite added under `tests/e2e/`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.2"
|
|
@@ -18,6 +18,24 @@ class ChangelogGenerator:
|
|
|
18
18
|
If from_ref is None, attempts to find the latest tag.
|
|
19
19
|
If no latest tag exists, falls back to the last 30 commits.
|
|
20
20
|
"""
|
|
21
|
+
if from_ref:
|
|
22
|
+
try:
|
|
23
|
+
self.git_ops.execute(f"rev-parse --verify {from_ref}")
|
|
24
|
+
except Exception:
|
|
25
|
+
raise ChangelogGeneratorError(f"Invalid starting revision: {from_ref}")
|
|
26
|
+
|
|
27
|
+
if to_ref:
|
|
28
|
+
try:
|
|
29
|
+
self.git_ops.execute(f"rev-parse --verify {to_ref}")
|
|
30
|
+
except Exception:
|
|
31
|
+
raise ChangelogGeneratorError(f"Invalid ending revision: {to_ref}")
|
|
32
|
+
|
|
33
|
+
# Check if HEAD exists first
|
|
34
|
+
try:
|
|
35
|
+
self.git_ops.execute("rev-parse --verify HEAD")
|
|
36
|
+
except Exception:
|
|
37
|
+
return ""
|
|
38
|
+
|
|
21
39
|
to_revision = to_ref or "HEAD"
|
|
22
40
|
from_revision = from_ref
|
|
23
41
|
|
|
@@ -29,8 +29,13 @@ class CommitGenerator:
|
|
|
29
29
|
raise NoStagedChangesError("No changes are staged for commit. Stage files first using 'git add'.")
|
|
30
30
|
|
|
31
31
|
staged_diff = self.git_ops.get_staged_diff()
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
has_content_changes = False
|
|
33
|
+
for line in staged_diff.splitlines():
|
|
34
|
+
if (line.startswith("+") and not line.startswith("+++")) or (line.startswith("-") and not line.startswith("---")):
|
|
35
|
+
has_content_changes = True
|
|
36
|
+
break
|
|
37
|
+
|
|
38
|
+
if not has_content_changes:
|
|
34
39
|
raise NoStagedChangesError("Staged diff is empty. Cannot generate commit message.")
|
|
35
40
|
|
|
36
41
|
# Format context
|
|
@@ -43,10 +43,11 @@ def ensure_ollama_model(base_url: str, model_name: str) -> None:
|
|
|
43
43
|
return
|
|
44
44
|
|
|
45
45
|
# 2. Prompt user and pull model
|
|
46
|
-
from ace.ui.display import console, spinner
|
|
46
|
+
from ace.ui.display import console, spinner, print_warning, print_success, print_error, print_info
|
|
47
47
|
from ace.ui.prompts import confirm
|
|
48
48
|
|
|
49
|
-
console.print(
|
|
49
|
+
console.print()
|
|
50
|
+
print_warning(f"Ollama model '{model_name}' is not downloaded locally.")
|
|
50
51
|
if confirm(f"Would you like Ace to automatically pull '{model_name}' from the Ollama registry?", default=True):
|
|
51
52
|
try:
|
|
52
53
|
url = f"{base_url.rstrip('/')}/api/pull"
|
|
@@ -58,12 +59,12 @@ def ensure_ollama_model(base_url: str, model_name: str) -> None:
|
|
|
58
59
|
with urllib.request.urlopen(req) as response:
|
|
59
60
|
res_data = json.loads(response.read().decode("utf-8"))
|
|
60
61
|
if res_data.get("status") == "success" or "success" in str(res_data):
|
|
61
|
-
|
|
62
|
+
print_success(f"Successfully downloaded '{model_name}'!\n")
|
|
62
63
|
else:
|
|
63
|
-
|
|
64
|
+
print_info(f"Ollama response: {res_data}\n")
|
|
64
65
|
except Exception as e:
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
print_error(f"Failed to pull model: {e}")
|
|
67
|
+
print_info(f"Please run 'ollama pull {model_name}' manually in your shell.\n")
|
|
67
68
|
|
|
68
69
|
def get_llm(offline_override: bool = False) -> BaseChatModel:
|
|
69
70
|
"""
|
|
@@ -138,9 +138,9 @@ def main(
|
|
|
138
138
|
r_level, r_desc, alt = SafetyChecker.analyze_command(cmd)
|
|
139
139
|
if r_level == "destructive":
|
|
140
140
|
highest_risk = "destructive"
|
|
141
|
-
risk_details.append(f"[bold red]Command:[/
|
|
141
|
+
risk_details.append(f"[bold red]Command:[/] {cmd}\n[bold red]Risk:[/] {r_desc}")
|
|
142
142
|
if alt:
|
|
143
|
-
safer_alts.append(f"[bold green]Safer Alternative:[/
|
|
143
|
+
safer_alts.append(f"[bold green]Safer Alternative:[/] {alt}")
|
|
144
144
|
elif r_level == "moderate" and highest_risk != "destructive":
|
|
145
145
|
highest_risk = "moderate"
|
|
146
146
|
|
|
@@ -191,8 +191,19 @@ def main(
|
|
|
191
191
|
raise typer.Exit(code=1)
|
|
192
192
|
else:
|
|
193
193
|
try:
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
import subprocess
|
|
195
|
+
import sys
|
|
196
|
+
args = cmd.split()[1:]
|
|
197
|
+
res_proc = subprocess.run(
|
|
198
|
+
[sys.executable, "-c", "from ace.cli import app; app()"] + args,
|
|
199
|
+
stdout=subprocess.PIPE,
|
|
200
|
+
stderr=subprocess.PIPE,
|
|
201
|
+
text=True,
|
|
202
|
+
encoding="utf-8"
|
|
203
|
+
)
|
|
204
|
+
if res_proc.returncode != 0:
|
|
205
|
+
raise Exception(res_proc.stderr or res_proc.stdout)
|
|
206
|
+
outputs.append(res_proc.stdout)
|
|
196
207
|
except Exception as e:
|
|
197
208
|
show_error_panel(f"Failed to execute command '{cmd}': {e}", "Execution Error")
|
|
198
209
|
raise typer.Exit(code=1)
|
|
@@ -207,7 +218,7 @@ def main(
|
|
|
207
218
|
|
|
208
219
|
# Summarization flow for read-only history queries
|
|
209
220
|
combined_output = "\n".join(outputs)
|
|
210
|
-
if highest_risk == "safe" and combined_output.strip():
|
|
221
|
+
if highest_risk == "safe" and combined_output.strip() and not any(c.startswith("ace ") for c in commands):
|
|
211
222
|
from ace.ai.history_analyzer import HistoryAnalyzer
|
|
212
223
|
from rich.markdown import Markdown
|
|
213
224
|
analyzer = HistoryAnalyzer(git_ops)
|
|
@@ -270,6 +281,8 @@ def commit_cmd(
|
|
|
270
281
|
with open(prepare, "w", encoding="utf-8") as f:
|
|
271
282
|
f.write(msg)
|
|
272
283
|
raise typer.Exit(code=0)
|
|
284
|
+
except (typer.Exit, typer.Abort):
|
|
285
|
+
raise
|
|
273
286
|
except NoStagedChangesError:
|
|
274
287
|
raise typer.Exit(code=0)
|
|
275
288
|
except Exception as e:
|
|
@@ -785,7 +798,8 @@ def changelog_cmd(
|
|
|
785
798
|
show_error_panel(f"{str(e)}\n\nRun [bold]ace setup[/bold] to configure your AI credentials.", "Configuration Error")
|
|
786
799
|
raise typer.Exit(code=1)
|
|
787
800
|
except Exception as e:
|
|
788
|
-
|
|
801
|
+
title = "Git Error" if "ChangelogGeneratorError" in str(type(e)) or "Cmd('git')" in str(e) or "Invalid starting revision" in str(e) or "Invalid ending revision" in str(e) else "AI Error"
|
|
802
|
+
show_error_panel(f"Failed to generate changelog: {e}", title)
|
|
789
803
|
raise typer.Exit(code=1)
|
|
790
804
|
|
|
791
805
|
# Show or write to file
|
|
@@ -1154,9 +1168,9 @@ def undo_cmd(
|
|
|
1154
1168
|
r_level, r_desc, alt = SafetyChecker.analyze_command(cmd)
|
|
1155
1169
|
if r_level == "destructive":
|
|
1156
1170
|
highest_risk = "destructive"
|
|
1157
|
-
risk_details.append(f"[bold red]Command:[/
|
|
1171
|
+
risk_details.append(f"[bold red]Command:[/] {cmd}\n[bold red]Risk:[/] {r_desc}")
|
|
1158
1172
|
if alt:
|
|
1159
|
-
safer_alts.append(f"[bold green]Safer Alternative:[/
|
|
1173
|
+
safer_alts.append(f"[bold green]Safer Alternative:[/] {alt}")
|
|
1160
1174
|
elif r_level == "moderate" and highest_risk != "destructive":
|
|
1161
1175
|
highest_risk = "moderate"
|
|
1162
1176
|
|
|
@@ -1251,7 +1265,8 @@ def pr_cmd(
|
|
|
1251
1265
|
with spinner(f"Generating PR description against base branch '{base}'..."):
|
|
1252
1266
|
pr_data = drafter.draft_pr(base, offline=offline)
|
|
1253
1267
|
except Exception as e:
|
|
1254
|
-
|
|
1268
|
+
title = "Git Error" if "Cmd('git')" in str(e) or "git log" in str(e) or "exit code" in str(e) else "AI Error"
|
|
1269
|
+
show_error_panel(f"Failed to generate PR description: {e}", title)
|
|
1255
1270
|
raise typer.Exit(code=1)
|
|
1256
1271
|
|
|
1257
1272
|
title = pr_data.get("title", "Pull Request")
|
|
@@ -1335,10 +1350,16 @@ def search_cmd(
|
|
|
1335
1350
|
|
|
1336
1351
|
# Show options
|
|
1337
1352
|
console.print("\n[bold]Select action:[/bold]")
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1353
|
+
def _key(k: str, desc: str) -> None:
|
|
1354
|
+
from rich.text import Text
|
|
1355
|
+
console.print(Text.assemble(
|
|
1356
|
+
(" ", ""), (f"[{k}]", "bold #00D5FF"), (f" {desc}", "#BDBDBD")
|
|
1357
|
+
))
|
|
1358
|
+
_key("d", "View diff of this commit")
|
|
1359
|
+
_key("c", "Checkout this commit (detached HEAD)")
|
|
1360
|
+
_key("b", "Create and switch to a new branch here")
|
|
1361
|
+
_key("s", "Skip / Quit")
|
|
1362
|
+
|
|
1342
1363
|
|
|
1343
1364
|
action = click.getchar().lower().strip()
|
|
1344
1365
|
console.print(action)
|
|
@@ -199,16 +199,18 @@ def show_dashboard(git_ops: GitOps, offline: bool = False):
|
|
|
199
199
|
console.print()
|
|
200
200
|
|
|
201
201
|
# ── Action menu ─────────────────────────────────────────────────────
|
|
202
|
-
menu = Table(show_header=False, box=None, padding=(0,
|
|
203
|
-
menu.add_column(
|
|
204
|
-
menu.add_column(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
menu.add_row("
|
|
211
|
-
menu.add_row("
|
|
202
|
+
menu = Table(show_header=False, box=None, padding=(0, 4), expand=False)
|
|
203
|
+
menu.add_column(min_width=22)
|
|
204
|
+
menu.add_column(min_width=22)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _row(key: str, label: str) -> Text:
|
|
208
|
+
return Text.assemble((f"[{key}]", "bold #00D5FF"), (f" {label}", "#BDBDBD"))
|
|
209
|
+
|
|
210
|
+
menu.add_row(_row("c", "AI Commit"), _row("r", "AI Code Review"))
|
|
211
|
+
menu.add_row(_row("u", "Smart Undo"), _row("p", "Plan Command (AI)"))
|
|
212
|
+
menu.add_row(_row("s", "Repo Stats"), _row("w", "Switch Repo"))
|
|
213
|
+
menu.add_row(_row("q", "Quit"), Text(""))
|
|
212
214
|
|
|
213
215
|
console.print(
|
|
214
216
|
Panel(menu, title="[bold white]Actions[/bold white]",
|
|
@@ -30,9 +30,65 @@ def print_warning(message: str) -> None:
|
|
|
30
30
|
"""Print a warning message."""
|
|
31
31
|
console.print(f" [warning]{_SYM_WARNING}[/warning] [warning]{message}[/warning]")
|
|
32
32
|
|
|
33
|
+
def clean_error_message(message: str) -> str:
|
|
34
|
+
"""Clean up common LLM API response dumps and traceback details into human-readable text."""
|
|
35
|
+
from rich.markup import escape
|
|
36
|
+
|
|
37
|
+
# Normalize/clean raw message string
|
|
38
|
+
msg_str = str(message).strip()
|
|
39
|
+
|
|
40
|
+
# Check for NVIDIA/OpenAI 504 gateway timeout
|
|
41
|
+
if "504" in msg_str or "Gateway Timeout" in msg_str:
|
|
42
|
+
return (
|
|
43
|
+
"API Server Error: [bold #FF1744]Gateway Timeout (504)[/bold #FF1744]\n\n"
|
|
44
|
+
"The cloud AI provider servers took too long to respond. This is a temporary server "
|
|
45
|
+
"overload. Please try again in a few moments, or run [bold]ace setup[/bold] to switch "
|
|
46
|
+
"to a local model (Ollama) or another provider."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Check for authentication errors
|
|
50
|
+
if "AuthenticationError" in msg_str or "invalid api key" in msg_str.lower() or "401" in msg_str:
|
|
51
|
+
return (
|
|
52
|
+
"API Authentication Error: [bold #FF1744]Invalid API Key[/bold #FF1744]\n\n"
|
|
53
|
+
"Please check that your API key is correct and active. Run [bold]ace setup[/bold] to reconfigure."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Check for rate limits
|
|
57
|
+
if "RateLimitError" in msg_str or "429" in msg_str or "rate limit exceeded" in msg_str.lower():
|
|
58
|
+
return (
|
|
59
|
+
"API Rate Limit Error: [bold #FF1744]Too Many Requests[/bold #FF1744]\n\n"
|
|
60
|
+
"You have hit the rate limit for this API provider. Please wait a moment before trying again."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# General Connection errors
|
|
64
|
+
if "ConnectionError" in msg_str or "Failed to establish a new connection" in msg_str or "timeout" in msg_str.lower():
|
|
65
|
+
return (
|
|
66
|
+
"API Connection Error: [bold #FF1744]Network Timeout[/bold #FF1744]\n\n"
|
|
67
|
+
"Could not connect to the AI API endpoint. Please check your internet connection and try again."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# If it's a raw dictionary dump of a response (like in NVIDIA / OpenAI SDK exception strings)
|
|
71
|
+
if "{'_content':" in msg_str or "'status_code':" in msg_str or "'_content_consumed':" in msg_str:
|
|
72
|
+
lines = msg_str.splitlines()
|
|
73
|
+
first_line = lines[0] if lines else ""
|
|
74
|
+
if "APIStatusError" in first_line:
|
|
75
|
+
first_line = first_line.split("APIStatusError:")[-1].strip()
|
|
76
|
+
|
|
77
|
+
import re
|
|
78
|
+
status_match = re.search(r"'status_code':\s*(\d+)", msg_str)
|
|
79
|
+
reason_match = re.search(r"'reason':\s*'([^']+)'", msg_str)
|
|
80
|
+
if status_match and reason_match:
|
|
81
|
+
return f"API Error: [bold #FF1744]{escape(reason_match.group(1))} ({status_match.group(1)})[/bold #FF1744]"
|
|
82
|
+
elif status_match:
|
|
83
|
+
return f"API Error: [bold #FF1744]Status Code {status_match.group(1)}[/bold #FF1744]"
|
|
84
|
+
return escape(first_line) if first_line else "An unexpected API error occurred."
|
|
85
|
+
|
|
86
|
+
# If it's a generic exception or string, escape it to prevent Rich markup parsing errors
|
|
87
|
+
return escape(msg_str)
|
|
88
|
+
|
|
33
89
|
def print_error(message: str) -> None:
|
|
34
90
|
"""Print an error message."""
|
|
35
|
-
err_console.print(f" [error]{_SYM_ERROR}[/error] [error]{message}[/error]")
|
|
91
|
+
err_console.print(f" [error]{_SYM_ERROR}[/error] [error]{clean_error_message(message)}[/error]")
|
|
36
92
|
|
|
37
93
|
|
|
38
94
|
# ─── Panels ──────────────────────────────────────────────────────────────────
|
|
@@ -57,8 +113,11 @@ def show_error_panel(message: str, title: str = "Error") -> None:
|
|
|
57
113
|
"""Show a styled red error panel."""
|
|
58
114
|
from rich.panel import Panel
|
|
59
115
|
from rich import box
|
|
116
|
+
|
|
117
|
+
cleaned_message = clean_error_message(message)
|
|
118
|
+
|
|
60
119
|
panel = Panel(
|
|
61
|
-
Text.from_markup(
|
|
120
|
+
Text.from_markup(cleaned_message),
|
|
62
121
|
title=f"[bold #FF1744] {_SYM_ERROR} {title}[/bold #FF1744]",
|
|
63
122
|
border_style="#FF1744",
|
|
64
123
|
box=box.ROUNDED,
|
|
@@ -13,7 +13,7 @@ def confirm(question: str, default: bool = True) -> bool:
|
|
|
13
13
|
hint = "[Y/n]" if default else "[y/N]"
|
|
14
14
|
console.print(
|
|
15
15
|
Text.assemble(
|
|
16
|
-
("
|
|
16
|
+
(" >> ", "bold #00D5FF"),
|
|
17
17
|
(question + " ", "bold white"),
|
|
18
18
|
(hint, "bold #666666"),
|
|
19
19
|
(" ", ""),
|
|
@@ -30,7 +30,8 @@ def confirm(question: str, default: bool = True) -> bool:
|
|
|
30
30
|
return False
|
|
31
31
|
if val in ("\r", "\n", ""):
|
|
32
32
|
return default
|
|
33
|
-
|
|
33
|
+
# Default to False for safety on unrecognized inputs
|
|
34
|
+
return False
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
def prompt_action(options: Dict[str, Tuple[str, str]], default_key: str = "\r") -> str:
|
|
@@ -13,8 +13,19 @@ class MockLLMHandler(BaseHTTPRequestHandler):
|
|
|
13
13
|
# Suppress request logging to keep output clean
|
|
14
14
|
pass
|
|
15
15
|
|
|
16
|
+
def do_GET(self):
|
|
17
|
+
if self.path == "/api/tags":
|
|
18
|
+
self.send_response(200)
|
|
19
|
+
self.send_header("Content-Type", "application/json")
|
|
20
|
+
self.end_headers()
|
|
21
|
+
self.wfile.write(json.dumps({"models": [{"name": "qwen2.5-coder:7b"}]}).encode('utf-8'))
|
|
22
|
+
else:
|
|
23
|
+
self.send_response(404)
|
|
24
|
+
self.end_headers()
|
|
25
|
+
|
|
16
26
|
def do_POST(self):
|
|
17
|
-
|
|
27
|
+
is_ollama = (self.path == "/api/chat")
|
|
28
|
+
if self.path == "/v1/chat/completions" or is_ollama:
|
|
18
29
|
content_length = int(self.headers['Content-Length'])
|
|
19
30
|
post_data = self.rfile.read(content_length)
|
|
20
31
|
payload = json.loads(post_data.decode('utf-8'))
|
|
@@ -69,7 +80,14 @@ class MockLLMHandler(BaseHTTPRequestHandler):
|
|
|
69
80
|
"risk_level": "destructive",
|
|
70
81
|
"alternatives": "git stash"
|
|
71
82
|
})
|
|
72
|
-
elif "
|
|
83
|
+
elif "config" in query_val:
|
|
84
|
+
response_content = json.dumps({
|
|
85
|
+
"commands": ["ace config"],
|
|
86
|
+
"explanation": "Show active configuration.",
|
|
87
|
+
"risk_level": "safe",
|
|
88
|
+
"alternatives": None
|
|
89
|
+
})
|
|
90
|
+
elif "invalid" in query_val or "unrelated" in query_val or "coffee" in query_val or "make me" in query_val:
|
|
73
91
|
response_content = json.dumps({
|
|
74
92
|
"commands": [],
|
|
75
93
|
"explanation": "I cannot parse this command.",
|
|
@@ -109,6 +127,10 @@ class MockLLMHandler(BaseHTTPRequestHandler):
|
|
|
109
127
|
"alternatives": None
|
|
110
128
|
})
|
|
111
129
|
|
|
130
|
+
# 3. Changelog Generator
|
|
131
|
+
elif "release coordinator and technical writer" in full_text or "Markdown changelog from the provided Git commit log" in full_text or "changelog" in full_text.lower():
|
|
132
|
+
response_content = "# Changelog\n\n## [1.0.0]\n\n### ✨ Features\n- Add mock feature\n\n### 🐛 Bug Fixes\n- Fix mock bug"
|
|
133
|
+
|
|
112
134
|
# 2. Commit Message Generator
|
|
113
135
|
elif "Conventional Commits" in full_text or "commit message" in full_text or "Staged Diff" in full_text:
|
|
114
136
|
if "one-line commit message" in full_text or "SIMPLE_COMMIT_SYSTEM_PROMPT" in full_text:
|
|
@@ -118,10 +140,6 @@ class MockLLMHandler(BaseHTTPRequestHandler):
|
|
|
118
140
|
else:
|
|
119
141
|
response_content = "feat(mock): add mock feature\n\n- Implement mock feature details\n- Add mock feature tests"
|
|
120
142
|
|
|
121
|
-
# 3. Changelog Generator
|
|
122
|
-
elif "release coordinator and technical writer" in full_text or "Markdown changelog from the provided Git commit log" in full_text:
|
|
123
|
-
response_content = "# Changelog\n\n## [1.0.0]\n\n### ✨ Features\n- Add mock feature\n\n### 🐛 Bug Fixes\n- Fix mock bug"
|
|
124
|
-
|
|
125
143
|
# 4. PR Drafter
|
|
126
144
|
elif "PR_SYSTEM_PROMPT" in full_text or "Pull Request (PR) description" in full_text:
|
|
127
145
|
response_content = json.dumps({
|
|
@@ -134,54 +152,64 @@ class MockLLMHandler(BaseHTTPRequestHandler):
|
|
|
134
152
|
response_content = "🩺 **Diagnostics Assessment**\n\nFound some issues.\n\n📋 **Recovery Plan**\n\n- Run git clean\n- Run git restore\n\n💡 **Prevention Tip**\n\nCommit more often."
|
|
135
153
|
|
|
136
154
|
# 6. Smart Undo
|
|
137
|
-
elif "
|
|
138
|
-
if "
|
|
139
|
-
response_content = json.dumps({
|
|
140
|
-
"commands": ["git restore --staged ."],
|
|
141
|
-
"explanation": "Unstage changes.",
|
|
142
|
-
"risk_level": "moderate",
|
|
143
|
-
"alternatives": None
|
|
144
|
-
})
|
|
145
|
-
elif "destructive" in full_text or "hard" in full_text:
|
|
146
|
-
response_content = json.dumps({
|
|
147
|
-
"commands": ["git reset --hard ORIG_HEAD"],
|
|
148
|
-
"explanation": "Destructively undo merge.",
|
|
149
|
-
"risk_level": "destructive",
|
|
150
|
-
"alternatives": "git stash"
|
|
151
|
-
})
|
|
152
|
-
elif "nothing" in full_text or "clean" in full_text:
|
|
155
|
+
elif "Active Operations:" in full_text or "recent Git reflog" in full_text:
|
|
156
|
+
if "No reflog available" in full_text or ("Staged Changes:\nNone" in full_text and "Unstaged Changes:\nNone" in full_text):
|
|
153
157
|
response_content = json.dumps({
|
|
154
158
|
"commands": [],
|
|
155
159
|
"explanation": "Nothing to undo.",
|
|
156
160
|
"risk_level": "safe",
|
|
157
161
|
"alternatives": None
|
|
158
162
|
})
|
|
159
|
-
|
|
163
|
+
elif "test_staged.txt" in full_text or "test.txt" in full_text:
|
|
164
|
+
response_content = json.dumps({
|
|
165
|
+
"commands": ["git restore --staged ."],
|
|
166
|
+
"explanation": "Unstage changes.",
|
|
167
|
+
"risk_level": "moderate",
|
|
168
|
+
"alternatives": None
|
|
169
|
+
})
|
|
170
|
+
elif "update commit" in full_text:
|
|
160
171
|
response_content = json.dumps({
|
|
161
172
|
"commands": ["git reset --soft HEAD~1"],
|
|
162
173
|
"explanation": "Undo last commit.",
|
|
163
174
|
"risk_level": "moderate",
|
|
164
175
|
"alternatives": None
|
|
165
176
|
})
|
|
177
|
+
else:
|
|
178
|
+
response_content = json.dumps({
|
|
179
|
+
"commands": ["git reset --hard ORIG_HEAD"],
|
|
180
|
+
"explanation": "Destructively undo merge.",
|
|
181
|
+
"risk_level": "destructive",
|
|
182
|
+
"alternatives": "git stash"
|
|
183
|
+
})
|
|
166
184
|
|
|
167
185
|
self.send_response(200)
|
|
168
186
|
self.send_header("Content-Type", "application/json")
|
|
169
187
|
self.end_headers()
|
|
170
188
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
"created": 1677652288,
|
|
175
|
-
"model": "mock-model",
|
|
176
|
-
"choices": [{
|
|
177
|
-
"index": 0,
|
|
189
|
+
if is_ollama:
|
|
190
|
+
response_payload = {
|
|
191
|
+
"model": "qwen2.5-coder:7b",
|
|
178
192
|
"message": {
|
|
179
193
|
"role": "assistant",
|
|
180
194
|
"content": response_content
|
|
181
195
|
},
|
|
182
|
-
"
|
|
183
|
-
}
|
|
184
|
-
|
|
196
|
+
"done": True
|
|
197
|
+
}
|
|
198
|
+
else:
|
|
199
|
+
response_payload = {
|
|
200
|
+
"id": "chatcmpl-mock",
|
|
201
|
+
"object": "chat.completion",
|
|
202
|
+
"created": 1677652288,
|
|
203
|
+
"model": "mock-model",
|
|
204
|
+
"choices": [{
|
|
205
|
+
"index": 0,
|
|
206
|
+
"message": {
|
|
207
|
+
"role": "assistant",
|
|
208
|
+
"content": response_content
|
|
209
|
+
},
|
|
210
|
+
"finish_reason": "stop"
|
|
211
|
+
}]
|
|
212
|
+
}
|
|
185
213
|
self.wfile.write(json.dumps(response_payload).encode('utf-8'))
|
|
186
214
|
else:
|
|
187
215
|
self.send_response(404)
|
|
@@ -232,13 +260,14 @@ def git_workspace(tmp_path, mock_llm_port):
|
|
|
232
260
|
cmd = [
|
|
233
261
|
sys.executable,
|
|
234
262
|
"-c",
|
|
235
|
-
"import sys, click; click.getchar = lambda: sys.stdin.read(1); from ace.cli import app; app()",
|
|
263
|
+
"import sys, click, getpass; click.getchar = lambda: sys.stdin.read(1); getpass.getpass = lambda prompt='', stream=None: sys.stdin.readline().rstrip('\\r\\n'); from ace.cli import app; app()",
|
|
236
264
|
] + args
|
|
237
265
|
env = os.environ.copy()
|
|
238
266
|
env["ACE_PROVIDER"] = "custom"
|
|
239
267
|
env["CUSTOM_API_BASE"] = f"http://127.0.0.1:{self.port}/v1"
|
|
240
268
|
env["CUSTOM_API_KEY"] = "mock-key"
|
|
241
269
|
env["CUSTOM_MODEL"] = "mock-model"
|
|
270
|
+
env["OLLAMA_URL"] = f"http://127.0.0.1:{self.port}"
|
|
242
271
|
env["HOME"] = str(self.home)
|
|
243
272
|
env["USERPROFILE"] = str(self.home)
|
|
244
273
|
|
|
@@ -165,8 +165,8 @@ def test_changelog_display(git_workspace):
|
|
|
165
165
|
|
|
166
166
|
res = git_workspace.run(["changelog"])
|
|
167
167
|
assert res.returncode == 0
|
|
168
|
-
assert "
|
|
169
|
-
assert "
|
|
168
|
+
assert "[1.0.0]" in res.stdout
|
|
169
|
+
assert "Features" in res.stdout
|
|
170
170
|
|
|
171
171
|
def test_changelog_output_file(git_workspace):
|
|
172
172
|
test_file = git_workspace.workspace / "test.txt"
|
|
@@ -178,9 +178,15 @@ def test_changelog_output_file(git_workspace):
|
|
|
178
178
|
res = git_workspace.run(["changelog", "-o", str(out_file)])
|
|
179
179
|
assert res.returncode == 0
|
|
180
180
|
assert out_file.exists()
|
|
181
|
-
assert "## [1.0.0]" in out_file.read_text()
|
|
181
|
+
assert "## [1.0.0]" in out_file.read_text(encoding="utf-8")
|
|
182
182
|
|
|
183
183
|
def test_changelog_range(git_workspace):
|
|
184
|
+
# Create initial commit so HEAD~1 exists
|
|
185
|
+
dummy_file = git_workspace.workspace / "dummy.txt"
|
|
186
|
+
dummy_file.write_text("dummy")
|
|
187
|
+
git_workspace.repo.index.add([str(dummy_file)])
|
|
188
|
+
git_workspace.repo.index.commit("initial")
|
|
189
|
+
|
|
184
190
|
test_file = git_workspace.workspace / "test.txt"
|
|
185
191
|
test_file.write_text("changelog content 3")
|
|
186
192
|
git_workspace.repo.index.add([str(test_file)])
|
|
@@ -188,7 +194,7 @@ def test_changelog_range(git_workspace):
|
|
|
188
194
|
|
|
189
195
|
res = git_workspace.run(["changelog", "--from", "HEAD~1", "--to", "HEAD"])
|
|
190
196
|
assert res.returncode == 0
|
|
191
|
-
assert "
|
|
197
|
+
assert "[1.0.0]" in res.stdout
|
|
192
198
|
|
|
193
199
|
def test_changelog_offline(git_workspace):
|
|
194
200
|
test_file = git_workspace.workspace / "test.txt"
|
|
@@ -198,7 +204,7 @@ def test_changelog_offline(git_workspace):
|
|
|
198
204
|
|
|
199
205
|
res = git_workspace.run(["changelog", "--offline"])
|
|
200
206
|
assert res.returncode == 0
|
|
201
|
-
assert "
|
|
207
|
+
assert "[1.0.0]" in res.stdout
|
|
202
208
|
|
|
203
209
|
def test_changelog_empty(git_workspace):
|
|
204
210
|
# Fresh repository with no commits
|
|
@@ -340,9 +346,15 @@ def test_undo_commit(git_workspace):
|
|
|
340
346
|
|
|
341
347
|
# Commit should be undone, leaving 1 commit and changes staged
|
|
342
348
|
assert len(list(git_workspace.repo.iter_commits())) == 1
|
|
343
|
-
assert "test.txt" in git_workspace.repo.index.diff("HEAD")
|
|
349
|
+
assert "test.txt" in [diff.a_path for diff in git_workspace.repo.index.diff("HEAD")]
|
|
344
350
|
|
|
345
351
|
def test_undo_staged(git_workspace):
|
|
352
|
+
# Create initial commit so HEAD exists
|
|
353
|
+
dummy_file = git_workspace.workspace / "dummy.txt"
|
|
354
|
+
dummy_file.write_text("dummy")
|
|
355
|
+
git_workspace.repo.index.add([str(dummy_file)])
|
|
356
|
+
git_workspace.repo.index.commit("initial")
|
|
357
|
+
|
|
346
358
|
test_file = git_workspace.workspace / "test_staged.txt"
|
|
347
359
|
test_file.write_text("staged content")
|
|
348
360
|
git_workspace.repo.index.add([str(test_file)])
|