opencontext-cli 0.1.0__tar.gz → 0.2.1b0__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.
- {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/PKG-INFO +1 -1
- opencontext_cli-0.2.1b0/opencontext_cli/commands/__init__.py +1 -0
- opencontext_cli-0.2.1b0/opencontext_cli/commands/ci_check_cmd.py +115 -0
- opencontext_cli-0.2.1b0/opencontext_cli/commands/config_cmd.py +242 -0
- opencontext_cli-0.2.1b0/opencontext_cli/commands/git_cmd.py +139 -0
- opencontext_cli-0.2.1b0/opencontext_cli/commands/hints_cmd.py +67 -0
- opencontext_cli-0.2.1b0/opencontext_cli/commands/kg_cmd.py +193 -0
- opencontext_cli-0.2.1b0/opencontext_cli/commands/plugin_cmd.py +366 -0
- opencontext_cli-0.2.1b0/opencontext_cli/commands/setup_cmd.py +346 -0
- opencontext_cli-0.2.1b0/opencontext_cli/commands/sync_cmd.py +187 -0
- opencontext_cli-0.2.1b0/opencontext_cli/commands/update_cmd.py +86 -0
- opencontext_cli-0.2.1b0/opencontext_cli/commands/verify_cmd.py +122 -0
- {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/opencontext_cli/main.py +510 -54
- {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/opencontext_cli.egg-info/PKG-INFO +1 -1
- opencontext_cli-0.2.1b0/opencontext_cli.egg-info/SOURCES.txt +22 -0
- {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/pyproject.toml +1 -1
- opencontext_cli-0.1.0/opencontext_cli.egg-info/SOURCES.txt +0 -11
- {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/LICENSE +0 -0
- {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/README.md +0 -0
- {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/opencontext_cli/__init__.py +0 -0
- {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/opencontext_cli.egg-info/dependency_links.txt +0 -0
- {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/opencontext_cli.egg-info/entry_points.txt +0 -0
- {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/opencontext_cli.egg-info/requires.txt +0 -0
- {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/opencontext_cli.egg-info/top_level.txt +0 -0
- {opencontext_cli-0.1.0 → opencontext_cli-0.2.1b0}/setup.cfg +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command modules for OpenContext."""
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""CI check CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from opencontext_core.dx.console_styles import console
|
|
9
|
+
from opencontext_core.quality.ci_checks import CheckRunner
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_ci_check_parser(subparsers: Any) -> None:
|
|
13
|
+
"""Add ci-check command parsers."""
|
|
14
|
+
check_parser = subparsers.add_parser("ci-check", help="CI check management.")
|
|
15
|
+
check_sub = check_parser.add_subparsers(dest="ci_check_command", required=True)
|
|
16
|
+
check_sub.add_parser("init", help="Initialize checks directory.")
|
|
17
|
+
check_sub.add_parser("list", help="List discovered checks.")
|
|
18
|
+
check_run = check_sub.add_parser("run", help="Run all checks.")
|
|
19
|
+
check_run.add_argument("--file", help="Run on specific file only.")
|
|
20
|
+
check_run.add_argument("--json", action="store_true")
|
|
21
|
+
check_create = check_sub.add_parser("create", help="Create a new check template.")
|
|
22
|
+
check_create.add_argument("name", help="Check name.")
|
|
23
|
+
check_create.add_argument("--description", default="", help="Check description.")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def handle_ci_check(args: Any) -> None:
|
|
27
|
+
"""Handle ci-check commands."""
|
|
28
|
+
command = args.ci_check_command
|
|
29
|
+
name = getattr(args, "name", None)
|
|
30
|
+
file = getattr(args, "file", None)
|
|
31
|
+
json_output = getattr(args, "json", False)
|
|
32
|
+
|
|
33
|
+
runner = CheckRunner()
|
|
34
|
+
|
|
35
|
+
if command == "init":
|
|
36
|
+
path = runner.init_checks_directory()
|
|
37
|
+
console.success(f"Initialized checks directory: {path}")
|
|
38
|
+
elif command == "list":
|
|
39
|
+
checks = runner.discover_checks()
|
|
40
|
+
if json_output:
|
|
41
|
+
data = [
|
|
42
|
+
{"name": c.name, "description": c.description, "severity": c.severity.value}
|
|
43
|
+
for c in checks
|
|
44
|
+
]
|
|
45
|
+
print(json.dumps(data, indent=2))
|
|
46
|
+
else:
|
|
47
|
+
console.header("CI Checks")
|
|
48
|
+
if not checks:
|
|
49
|
+
console.dim("No checks found. Run 'opencontext ci-check init'")
|
|
50
|
+
else:
|
|
51
|
+
console.table(
|
|
52
|
+
"Discovered Checks",
|
|
53
|
+
["Name", "Severity", "Description"],
|
|
54
|
+
[[c.name, c.severity.value, c.description] for c in checks],
|
|
55
|
+
)
|
|
56
|
+
elif command == "run":
|
|
57
|
+
files = [file] if file else None
|
|
58
|
+
with console.progress("Running checks...") as progress:
|
|
59
|
+
task = progress.add_task("Running checks...", total=None)
|
|
60
|
+
results = runner.run_all_checks(files)
|
|
61
|
+
progress.update(task, completed=True)
|
|
62
|
+
report = runner.generate_report(results)
|
|
63
|
+
if json_output:
|
|
64
|
+
print(json.dumps(report, indent=2))
|
|
65
|
+
else:
|
|
66
|
+
_display_check_report(report)
|
|
67
|
+
elif command == "create" and name:
|
|
68
|
+
template = runner.create_check_template(name, "Custom check")
|
|
69
|
+
console.print(template)
|
|
70
|
+
else:
|
|
71
|
+
console.error(f"Unknown ci-check command: {command}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _display_check_report(report: dict[str, Any]) -> None:
|
|
75
|
+
"""Display a formatted check report."""
|
|
76
|
+
summary = report.get("summary", {})
|
|
77
|
+
total = summary.get("total_checks", 0)
|
|
78
|
+
passed = summary.get("passed", 0)
|
|
79
|
+
failed = summary.get("failed", 0)
|
|
80
|
+
warnings = summary.get("warnings", 0)
|
|
81
|
+
errors = summary.get("errors", 0)
|
|
82
|
+
success = summary.get("success", False)
|
|
83
|
+
|
|
84
|
+
console.header("Check Report")
|
|
85
|
+
|
|
86
|
+
if success:
|
|
87
|
+
console.success(f"All {total} checks passed")
|
|
88
|
+
else:
|
|
89
|
+
console.error(f"{failed}/{total} checks failed")
|
|
90
|
+
|
|
91
|
+
console.table(
|
|
92
|
+
"Summary",
|
|
93
|
+
["Metric", "Count"],
|
|
94
|
+
[
|
|
95
|
+
["Total", str(total)],
|
|
96
|
+
["Passed", str(passed)],
|
|
97
|
+
["Failed", str(failed)],
|
|
98
|
+
["Warnings", str(warnings)],
|
|
99
|
+
["Errors", str(errors)],
|
|
100
|
+
],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Show failed results
|
|
104
|
+
failed_results = [r for r in report.get("results", []) if r["status"] != "passed"]
|
|
105
|
+
if failed_results:
|
|
106
|
+
console.section("Failed Checks")
|
|
107
|
+
for r in failed_results:
|
|
108
|
+
severity_color = "#FF6F91" if r["severity"] in ("error", "critical") else "#FFC75F"
|
|
109
|
+
msg = f" [bold {severity_color}]{r['severity'].upper()}[/]"
|
|
110
|
+
msg += f" {r['check']}: {r['message']}"
|
|
111
|
+
console.print(msg)
|
|
112
|
+
if r.get("file"):
|
|
113
|
+
console.print(f" [dim]File: {r['file']}:{r.get('line', 'N/A')}[/]")
|
|
114
|
+
if r.get("suggestion"):
|
|
115
|
+
console.print(f" [dim]Suggestion: {r['suggestion']}[/]")
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Configuration management CLI commands.
|
|
2
|
+
|
|
3
|
+
Provides wizard, show, reset, reconfigure, backup,
|
|
4
|
+
restore, and cleanup subcommands for managing
|
|
5
|
+
OpenContext user preferences.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from opencontext_core.state import ConfigBackupManager
|
|
15
|
+
from opencontext_core.user_prefs import UserConfigStore
|
|
16
|
+
from opencontext_core.wizard import (
|
|
17
|
+
reconfigure,
|
|
18
|
+
reset_config,
|
|
19
|
+
run_wizard,
|
|
20
|
+
show_config,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def add_config_parser(subparsers: Any) -> None:
|
|
25
|
+
"""Add config command parsers."""
|
|
26
|
+
|
|
27
|
+
config_parser = subparsers.add_parser("config", help="Manage OpenContext configuration.")
|
|
28
|
+
config_sub = config_parser.add_subparsers(dest="config_command", required=True)
|
|
29
|
+
|
|
30
|
+
# Wizard
|
|
31
|
+
wizard_parser = config_sub.add_parser("wizard", help="Run configuration wizard.")
|
|
32
|
+
wizard_parser.add_argument(
|
|
33
|
+
"--non-interactive", action="store_true", help="Use defaults without prompts."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Show
|
|
37
|
+
config_sub.add_parser("show", help="Display current configuration.")
|
|
38
|
+
|
|
39
|
+
# Reset
|
|
40
|
+
config_sub.add_parser("reset", help="Reset to factory defaults.")
|
|
41
|
+
|
|
42
|
+
# Reconfigure section
|
|
43
|
+
reconf_parser = config_sub.add_parser("reconfigure", help="Reconfigure a specific section.")
|
|
44
|
+
reconf_parser.add_argument(
|
|
45
|
+
"section",
|
|
46
|
+
choices=["security", "features", "tokens", "agents", "plugins"],
|
|
47
|
+
help="Section to reconfigure.",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Set individual values
|
|
51
|
+
set_parser = config_sub.add_parser("set", help="Set a configuration value.")
|
|
52
|
+
set_parser.add_argument("key", help="Configuration key (dot notation).")
|
|
53
|
+
set_parser.add_argument("value", help="Value to set.")
|
|
54
|
+
|
|
55
|
+
# Get individual values
|
|
56
|
+
get_parser = config_sub.add_parser("get", help="Get a configuration value.")
|
|
57
|
+
get_parser.add_argument("key", help="Configuration key (dot notation).")
|
|
58
|
+
|
|
59
|
+
# Backup
|
|
60
|
+
config_sub.add_parser("backup", help="Create a manual backup of configuration.")
|
|
61
|
+
|
|
62
|
+
# List backups
|
|
63
|
+
config_sub.add_parser("backups", help="List saved configuration backups.")
|
|
64
|
+
|
|
65
|
+
# Restore
|
|
66
|
+
restore_parser = config_sub.add_parser("restore", help="Restore configuration from a backup.")
|
|
67
|
+
restore_parser.add_argument("id", help="Backup ID to restore.")
|
|
68
|
+
|
|
69
|
+
# Cleanup old backups
|
|
70
|
+
cleanup_parser = config_sub.add_parser("cleanup", help="Remove old backups.")
|
|
71
|
+
cleanup_parser.add_argument(
|
|
72
|
+
"--keep-days", type=int, default=30, help="Keep backups newer than this many days."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def handle_config(args: Any) -> None:
|
|
77
|
+
"""Handle config commands."""
|
|
78
|
+
|
|
79
|
+
command = args.config_command
|
|
80
|
+
|
|
81
|
+
if command == "wizard":
|
|
82
|
+
run_wizard(non_interactive=getattr(args, "non_interactive", False))
|
|
83
|
+
elif command == "show":
|
|
84
|
+
show_config()
|
|
85
|
+
elif command == "reset":
|
|
86
|
+
reset_config()
|
|
87
|
+
elif command == "reconfigure":
|
|
88
|
+
reconfigure(args.section)
|
|
89
|
+
elif command == "set":
|
|
90
|
+
_config_set(args.key, args.value)
|
|
91
|
+
elif command == "get":
|
|
92
|
+
_config_get(args.key)
|
|
93
|
+
elif command == "backup":
|
|
94
|
+
_config_backup()
|
|
95
|
+
elif command == "backups":
|
|
96
|
+
_config_backups()
|
|
97
|
+
elif command == "restore":
|
|
98
|
+
_config_restore(args.id)
|
|
99
|
+
elif command == "cleanup":
|
|
100
|
+
_config_cleanup(args.keep_days)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _config_set(key: str, value: str) -> None:
|
|
104
|
+
"""Set a config value by key."""
|
|
105
|
+
|
|
106
|
+
store = UserConfigStore()
|
|
107
|
+
prefs = store.load()
|
|
108
|
+
|
|
109
|
+
# Simple key-value mapping
|
|
110
|
+
key_map: dict[str, tuple[str, type]] = {
|
|
111
|
+
"security_mode": ("security_mode", str),
|
|
112
|
+
"token_budget": ("default_token_budget", int),
|
|
113
|
+
"max_input_tokens": ("max_input_tokens", int),
|
|
114
|
+
"check_updates": ("check_updates", bool),
|
|
115
|
+
"auto_optimize": ("learning_auto_optimize", bool),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if key in key_map:
|
|
119
|
+
attr_name, attr_type = key_map[key]
|
|
120
|
+
if attr_type is bool:
|
|
121
|
+
parsed = value.lower() in ("true", "1", "yes", "on")
|
|
122
|
+
elif attr_type is int:
|
|
123
|
+
parsed = int(value)
|
|
124
|
+
else:
|
|
125
|
+
parsed = value
|
|
126
|
+
setattr(prefs, attr_name, parsed)
|
|
127
|
+
store.save(prefs)
|
|
128
|
+
print(f"Set {key} = {parsed}")
|
|
129
|
+
else:
|
|
130
|
+
print(f"Unknown key: {key}")
|
|
131
|
+
print(f"Available: {', '.join(key_map.keys())}")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _config_get(key: str) -> None:
|
|
135
|
+
"""Get a config value by key."""
|
|
136
|
+
|
|
137
|
+
store = UserConfigStore()
|
|
138
|
+
prefs = store.load()
|
|
139
|
+
|
|
140
|
+
key_map = {
|
|
141
|
+
"security_mode": prefs.security_mode,
|
|
142
|
+
"token_budget": prefs.default_token_budget,
|
|
143
|
+
"max_input_tokens": prefs.max_input_tokens,
|
|
144
|
+
"check_updates": prefs.check_updates,
|
|
145
|
+
"auto_optimize": prefs.learning_auto_optimize,
|
|
146
|
+
"first_run": prefs.first_run,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if key in key_map:
|
|
150
|
+
print(f"{key} = {key_map[key]}")
|
|
151
|
+
else:
|
|
152
|
+
print(f"Unknown key: {key}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _config_backup() -> None:
|
|
156
|
+
"""Create a manual backup."""
|
|
157
|
+
|
|
158
|
+
backup_id = ConfigBackupManager.create_backup(description="manual")
|
|
159
|
+
print(f" ✓ Backup created: {backup_id}")
|
|
160
|
+
print(f" Location: {ConfigBackupManager.BACKUP_DIR / backup_id}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _config_backups() -> None:
|
|
164
|
+
"""List backups."""
|
|
165
|
+
|
|
166
|
+
backups = ConfigBackupManager.list_backups()
|
|
167
|
+
if not backups:
|
|
168
|
+
print(" No backups found.")
|
|
169
|
+
print(f" Backup directory: {ConfigBackupManager.BACKUP_DIR}")
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
print()
|
|
173
|
+
print(f" {'Backup ID':<30} {'Timestamp':<25} {'Description':<20} {'Files'}")
|
|
174
|
+
print(f" {'─' * 30} {'─' * 25} {'─' * 20} {'─' * 20}")
|
|
175
|
+
for b in backups:
|
|
176
|
+
files_str = ", ".join(b.files) if b.files else "—"
|
|
177
|
+
print(f" {b.id:<30} {b.timestamp:<25} {b.description:<20} {files_str}")
|
|
178
|
+
print(f"\n {len(backups)} backup(s) available")
|
|
179
|
+
print("\n Restore: opencontext config restore <id>")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _config_restore(backup_id: str) -> None:
|
|
183
|
+
"""Restore from a backup."""
|
|
184
|
+
|
|
185
|
+
if ConfigBackupManager.restore_backup(backup_id):
|
|
186
|
+
print(f" ✓ Restored from backup: {backup_id}")
|
|
187
|
+
else:
|
|
188
|
+
print(f" ✗ Backup not found: {backup_id}")
|
|
189
|
+
print(" List available: opencontext config backups")
|
|
190
|
+
sys.exit(1)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _config_cleanup(keep_days: int) -> None:
|
|
194
|
+
"""Clean up old backups beyond keep_days."""
|
|
195
|
+
|
|
196
|
+
from datetime import datetime, timedelta
|
|
197
|
+
|
|
198
|
+
backups = ConfigBackupManager.list_backups()
|
|
199
|
+
cutoff = datetime.now() - timedelta(days=keep_days)
|
|
200
|
+
removed = 0
|
|
201
|
+
|
|
202
|
+
for b in backups:
|
|
203
|
+
try:
|
|
204
|
+
ts = datetime.strptime(b.timestamp, "%Y%m%dT%H%M%S")
|
|
205
|
+
if ts < cutoff:
|
|
206
|
+
backup_dir = ConfigBackupManager.BACKUP_DIR / b.id
|
|
207
|
+
if backup_dir.exists():
|
|
208
|
+
import shutil
|
|
209
|
+
|
|
210
|
+
shutil.rmtree(backup_dir)
|
|
211
|
+
removed += 1
|
|
212
|
+
except (ValueError, OSError):
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
# Rebuild index
|
|
216
|
+
[b for b in backups if b.id not in [r.id for r in ConfigBackupManager.list_backups()]]
|
|
217
|
+
# Actually let's rebuild from disk
|
|
218
|
+
index = []
|
|
219
|
+
for entry_dir in sorted(ConfigBackupManager.BACKUP_DIR.iterdir()):
|
|
220
|
+
if entry_dir.is_dir() and entry_dir.name.startswith("backup-"):
|
|
221
|
+
try:
|
|
222
|
+
ts = entry_dir.name.replace("backup-", "")
|
|
223
|
+
desc = "auto-pre-change" # rough default
|
|
224
|
+
files = []
|
|
225
|
+
for f in entry_dir.iterdir():
|
|
226
|
+
if f.is_file():
|
|
227
|
+
files.append(f.name)
|
|
228
|
+
index.append(
|
|
229
|
+
{
|
|
230
|
+
"id": entry_dir.name,
|
|
231
|
+
"timestamp": ts,
|
|
232
|
+
"description": desc,
|
|
233
|
+
"files": files,
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
except OSError:
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
ConfigBackupManager.INDEX_FILE.write_text(json.dumps(index, indent=2), encoding="utf-8")
|
|
240
|
+
|
|
241
|
+
print(f" ✓ Removed {removed} backup(s) older than {keep_days} days")
|
|
242
|
+
print(f" {len(index)} backup(s) remaining")
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Git context CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from opencontext_core.dx.console_styles import console
|
|
9
|
+
from opencontext_core.indexing.git_context import GitContextProvider
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_git_parser(subparsers: Any) -> None:
|
|
13
|
+
"""Add git command parsers."""
|
|
14
|
+
git_parser = subparsers.add_parser("git", help="Git context and history.")
|
|
15
|
+
git_sub = git_parser.add_subparsers(dest="git_command", required=True)
|
|
16
|
+
git_sub.add_parser("status", help="Show git repository stats.")
|
|
17
|
+
git_history = git_sub.add_parser("history", help="Show git history for a file.")
|
|
18
|
+
git_history.add_argument("file", help="File path.")
|
|
19
|
+
git_history.add_argument("--json", action="store_true")
|
|
20
|
+
git_recent = git_sub.add_parser("recent", help="Show recent changes.")
|
|
21
|
+
git_recent.add_argument("--days", type=int, default=7)
|
|
22
|
+
git_recent.add_argument("--max-commits", type=int, default=20)
|
|
23
|
+
git_recent.add_argument("--json", action="store_true")
|
|
24
|
+
git_blame = git_sub.add_parser("blame", help="Show blame for file lines.")
|
|
25
|
+
git_blame.add_argument("file", help="File path.")
|
|
26
|
+
git_blame.add_argument("--start", type=int, default=1)
|
|
27
|
+
git_blame.add_argument("--end", type=int, default=10)
|
|
28
|
+
git_blame.add_argument("--json", action="store_true")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def handle_git(args: Any) -> None:
|
|
32
|
+
"""Handle git commands."""
|
|
33
|
+
command = args.git_command
|
|
34
|
+
file = getattr(args, "file", None)
|
|
35
|
+
start = getattr(args, "start", 1)
|
|
36
|
+
end = getattr(args, "end", 10)
|
|
37
|
+
days = getattr(args, "days", 7)
|
|
38
|
+
max_commits = getattr(args, "max_commits", 20)
|
|
39
|
+
json_output = getattr(args, "json", False)
|
|
40
|
+
|
|
41
|
+
provider = GitContextProvider()
|
|
42
|
+
if not provider.available:
|
|
43
|
+
console.error("Not a git repository")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
if command == "status":
|
|
47
|
+
stats = provider.get_repo_stats()
|
|
48
|
+
if json_output:
|
|
49
|
+
print(json.dumps(stats, indent=2))
|
|
50
|
+
else:
|
|
51
|
+
console.header("Git Repository Stats")
|
|
52
|
+
console.print(_format_git_status(stats))
|
|
53
|
+
elif command == "history" and file:
|
|
54
|
+
info = provider.get_file_info(file)
|
|
55
|
+
if info:
|
|
56
|
+
if json_output:
|
|
57
|
+
data = {
|
|
58
|
+
"path": info.path,
|
|
59
|
+
"last_modified": info.last_modified.isoformat() if info.last_modified else None,
|
|
60
|
+
"last_author": info.last_author,
|
|
61
|
+
"commit_count": info.commit_count,
|
|
62
|
+
"lines_added": info.lines_added,
|
|
63
|
+
"lines_removed": info.lines_removed,
|
|
64
|
+
"top_authors": info.top_authors,
|
|
65
|
+
}
|
|
66
|
+
print(json.dumps(data, indent=2))
|
|
67
|
+
else:
|
|
68
|
+
console.header(f"Git History: {file}")
|
|
69
|
+
console.print(_format_git_history(info))
|
|
70
|
+
else:
|
|
71
|
+
console.error(f"Could not get history for {file}")
|
|
72
|
+
elif command == "recent":
|
|
73
|
+
diffs = provider.get_recent_changes(days=days, max_commits=max_commits)
|
|
74
|
+
if json_output:
|
|
75
|
+
data = [
|
|
76
|
+
{
|
|
77
|
+
"commit_hash": d.commit_hash,
|
|
78
|
+
"author": d.author,
|
|
79
|
+
"date": d.date.isoformat(),
|
|
80
|
+
"message": d.message,
|
|
81
|
+
"files_changed": d.files_changed,
|
|
82
|
+
}
|
|
83
|
+
for d in diffs
|
|
84
|
+
]
|
|
85
|
+
print(json.dumps(data, indent=2))
|
|
86
|
+
else:
|
|
87
|
+
console.header(f"Recent Changes (last {days} days)")
|
|
88
|
+
console.print(_format_git_recent(diffs))
|
|
89
|
+
elif command == "blame" and file:
|
|
90
|
+
lines = provider.get_blame_for_symbol(file, start, end)
|
|
91
|
+
if json_output:
|
|
92
|
+
print(json.dumps(lines, indent=2))
|
|
93
|
+
else:
|
|
94
|
+
console.header(f"Blame: {file} (lines {start}-{end})")
|
|
95
|
+
console.print(_format_git_blame(lines))
|
|
96
|
+
else:
|
|
97
|
+
console.error(f"Unknown git command: {command}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _format_git_status(stats: dict[str, Any]) -> str:
|
|
101
|
+
if not stats.get("available"):
|
|
102
|
+
return "Not a git repository."
|
|
103
|
+
lines = [
|
|
104
|
+
f"Commits: {stats.get('total_commits', 'N/A')}",
|
|
105
|
+
f"Contributors: {stats.get('contributors', 'N/A')}",
|
|
106
|
+
f"Branches: {stats.get('branches', 'N/A')}",
|
|
107
|
+
]
|
|
108
|
+
return "\n".join(lines)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _format_git_history(info: Any) -> str:
|
|
112
|
+
lines = [
|
|
113
|
+
f"File: {info.path}",
|
|
114
|
+
f"Commits: {info.commit_count}",
|
|
115
|
+
f"Last author: {info.last_author or 'N/A'}",
|
|
116
|
+
f"Last modified: {info.last_modified.isoformat() if info.last_modified else 'N/A'}",
|
|
117
|
+
f"Lines added/removed: +{info.lines_added}/-{info.lines_removed}",
|
|
118
|
+
f"Top authors: {', '.join(info.top_authors) if info.top_authors else 'N/A'}",
|
|
119
|
+
]
|
|
120
|
+
return "\n".join(lines)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _format_git_recent(diffs: list[Any]) -> str:
|
|
124
|
+
lines = []
|
|
125
|
+
for d in diffs:
|
|
126
|
+
lines.append(f"{d.commit_hash[:8]} {d.author} {d.date.isoformat()}")
|
|
127
|
+
lines.append(f" {d.message}")
|
|
128
|
+
lines.append(f" Files: {', '.join(d.files_changed[:5])}")
|
|
129
|
+
lines.append("")
|
|
130
|
+
return "\n".join(lines)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _format_git_blame(lines: list[dict[str, Any]]) -> str:
|
|
134
|
+
out = []
|
|
135
|
+
for line in lines:
|
|
136
|
+
author = line.get("author", "?")
|
|
137
|
+
code = line.get("code", "")
|
|
138
|
+
out.append(f"{author:20} {code}")
|
|
139
|
+
return "\n".join(out)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Agent hints CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from opencontext_core.dx.agent_hints import AgentHintsManager
|
|
9
|
+
from opencontext_core.dx.console_styles import console
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_hints_parser(subparsers: Any) -> None:
|
|
13
|
+
"""Add hints command parsers."""
|
|
14
|
+
hints_parser = subparsers.add_parser("hints", help="Agent hints management.")
|
|
15
|
+
hints_sub = hints_parser.add_subparsers(dest="hints_command", required=True)
|
|
16
|
+
hints_sub.add_parser("init", help="Initialize .opencontexthints file.")
|
|
17
|
+
hints_sub.add_parser("show", help="Show combined hints.")
|
|
18
|
+
hints_sub.add_parser("validate", help="Validate hints files.")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def handle_hints(args: Any) -> None:
|
|
22
|
+
"""Handle hints commands."""
|
|
23
|
+
command = args.hints_command
|
|
24
|
+
json_output = getattr(args, "json", False)
|
|
25
|
+
|
|
26
|
+
manager = AgentHintsManager()
|
|
27
|
+
|
|
28
|
+
if command == "init":
|
|
29
|
+
path = manager.init_hints_file()
|
|
30
|
+
if path:
|
|
31
|
+
console.success(f"Created {path}")
|
|
32
|
+
console.info("Edit the file to add your project conventions")
|
|
33
|
+
else:
|
|
34
|
+
console.warning(".opencontexthints already exists")
|
|
35
|
+
elif command == "show":
|
|
36
|
+
hints = manager.get_all_hints()
|
|
37
|
+
if hints:
|
|
38
|
+
ctx = manager.to_context_string(hints)
|
|
39
|
+
if json_output:
|
|
40
|
+
print(json.dumps({"hints": ctx}, indent=2))
|
|
41
|
+
else:
|
|
42
|
+
console.header("Agent Hints")
|
|
43
|
+
console.panel(ctx, title=hints.project_name or "Project Hints")
|
|
44
|
+
else:
|
|
45
|
+
console.warning("No hints found. Run 'opencontext hints init' to create them.")
|
|
46
|
+
elif command == "validate":
|
|
47
|
+
files = manager.discover_hints()
|
|
48
|
+
valid = []
|
|
49
|
+
invalid = []
|
|
50
|
+
for f in files:
|
|
51
|
+
parsed = manager.parse_hints_file(f)
|
|
52
|
+
if parsed:
|
|
53
|
+
valid.append(str(f))
|
|
54
|
+
else:
|
|
55
|
+
invalid.append(str(f))
|
|
56
|
+
if json_output:
|
|
57
|
+
print(json.dumps({"valid": valid, "invalid": invalid, "total": len(files)}, indent=2))
|
|
58
|
+
else:
|
|
59
|
+
console.header("Hints Validation")
|
|
60
|
+
console.success(f"Valid: {len(valid)}")
|
|
61
|
+
if invalid:
|
|
62
|
+
console.error(f"Invalid: {len(invalid)}")
|
|
63
|
+
for f in invalid:
|
|
64
|
+
console.print(f" [dim]✗ {f}[/]")
|
|
65
|
+
console.info(f"Total files checked: {len(files)}")
|
|
66
|
+
else:
|
|
67
|
+
console.error(f"Unknown hints command: {command}")
|