opencontext-cli 0.4.0b0__tar.gz → 1.0.0__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.4.0b0 → opencontext_cli-1.0.0}/PKG-INFO +3 -2
- opencontext_cli-1.0.0/opencontext_cli/commands/benchmark_cmd.py +162 -0
- opencontext_cli-1.0.0/opencontext_cli/commands/bridges_cmd.py +151 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/ci_check_cmd.py +3 -1
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/config_cmd.py +13 -10
- opencontext_cli-1.0.0/opencontext_cli/commands/extension_cmd.py +145 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/git_cmd.py +5 -3
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/hints_cmd.py +7 -5
- opencontext_cli-1.0.0/opencontext_cli/commands/kg_cmd.py +1056 -0
- opencontext_cli-1.0.0/opencontext_cli/commands/menu_cmd.py +653 -0
- opencontext_cli-1.0.0/opencontext_cli/commands/privacy_cmd.py +260 -0
- opencontext_cli-1.0.0/opencontext_cli/commands/review_cmd.py +221 -0
- opencontext_cli-1.0.0/opencontext_cli/commands/routes_cmd.py +81 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/setup_cmd.py +155 -80
- opencontext_cli-1.0.0/opencontext_cli/commands/skill_cmd.py +148 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/sync_cmd.py +130 -12
- opencontext_cli-1.0.0/opencontext_cli/commands/telemetry_cmd.py +74 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/update_cmd.py +14 -1
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/main.py +893 -618
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/PKG-INFO +3 -2
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/SOURCES.txt +8 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/requires.txt +1 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/pyproject.toml +3 -2
- opencontext_cli-0.4.0b0/opencontext_cli/commands/kg_cmd.py +0 -193
- opencontext_cli-0.4.0b0/opencontext_cli/commands/menu_cmd.py +0 -442
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/LICENSE +0 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/README.md +0 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/__init__.py +0 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/__main__.py +0 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/__init__.py +0 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/plugin_cmd.py +0 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli/commands/verify_cmd.py +0 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/dependency_links.txt +0 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/entry_points.txt +0 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/opencontext_cli.egg-info/top_level.txt +0 -0
- {opencontext_cli-0.4.0b0 → opencontext_cli-1.0.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencontext-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: CLI adapter for OpenContext Runtime
|
|
5
5
|
Author: OpenContext Runtime maintainers
|
|
6
6
|
License-Expression: MIT
|
|
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://github.com/CesarMSFelipe/OpenContext-Runtime
|
|
|
9
9
|
Project-URL: Issues, https://github.com/CesarMSFelipe/OpenContext-Runtime/issues
|
|
10
10
|
Project-URL: Source, https://github.com/CesarMSFelipe/OpenContext-Runtime
|
|
11
11
|
Keywords: ai,llm,context-engineering,cli,security
|
|
12
|
-
Classifier: Development Status ::
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Environment :: Console
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -21,6 +21,7 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: opencontext-core>=0.1.0
|
|
23
23
|
Requires-Dist: opencontext-profiles>=0.1.0
|
|
24
|
+
Requires-Dist: argcomplete>=3.0.0
|
|
24
25
|
Dynamic: license-file
|
|
25
26
|
|
|
26
27
|
# opencontext-cli
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Benchmark CLI commands: run, list, and compare benchmarks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from opencontext_core.dx.console_styles import console
|
|
10
|
+
from opencontext_core.evaluation.benchmark_suite import (
|
|
11
|
+
BenchmarkSuite,
|
|
12
|
+
format_benchmark_report_markdown,
|
|
13
|
+
format_benchmark_result,
|
|
14
|
+
format_benchmark_result_json,
|
|
15
|
+
load_last_result,
|
|
16
|
+
save_result,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def add_benchmark_parser(subparsers: Any) -> None:
|
|
21
|
+
"""Add benchmark command parsers."""
|
|
22
|
+
bm_parser = subparsers.add_parser("benchmark", help="Run and manage benchmarks.")
|
|
23
|
+
bm_sub = bm_parser.add_subparsers(dest="benchmark_command", required=True)
|
|
24
|
+
|
|
25
|
+
# benchmark list
|
|
26
|
+
list_parser = bm_sub.add_parser("list", help="List available benchmark cases.")
|
|
27
|
+
list_parser.add_argument("--category", default=None, help="Filter by category.")
|
|
28
|
+
|
|
29
|
+
# benchmark run
|
|
30
|
+
run_parser = bm_sub.add_parser("run", help="Run benchmark cases.")
|
|
31
|
+
run_parser.add_argument("--case", default=None, help="Specific case ID to run.")
|
|
32
|
+
run_parser.add_argument("--category", default=None, help="Filter by category.")
|
|
33
|
+
run_parser.add_argument(
|
|
34
|
+
"--format",
|
|
35
|
+
default="text",
|
|
36
|
+
choices=["text", "json", "markdown"],
|
|
37
|
+
help="Output format.",
|
|
38
|
+
)
|
|
39
|
+
run_parser.add_argument("--output", default=None, help="Output file (for markdown).")
|
|
40
|
+
run_parser.add_argument("--save", action="store_true", help="Save results.")
|
|
41
|
+
|
|
42
|
+
# benchmark compare
|
|
43
|
+
compare_parser = bm_sub.add_parser("compare", help="Compare against last baseline.")
|
|
44
|
+
compare_parser.add_argument(
|
|
45
|
+
"--output", default=None, help="Output file for markdown comparison."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def handle_benchmark(args: Any) -> None:
|
|
50
|
+
"""Handle benchmark commands."""
|
|
51
|
+
command = args.benchmark_command
|
|
52
|
+
|
|
53
|
+
if command == "list":
|
|
54
|
+
_handle_list(args)
|
|
55
|
+
elif command == "run":
|
|
56
|
+
_handle_run(args)
|
|
57
|
+
elif command == "compare":
|
|
58
|
+
_handle_compare(args)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _handle_list(args: Any) -> None:
|
|
62
|
+
"""List available benchmark cases."""
|
|
63
|
+
suite = BenchmarkSuite()
|
|
64
|
+
cases = suite.list_cases(category=args.category)
|
|
65
|
+
|
|
66
|
+
if not cases:
|
|
67
|
+
console.print("[yellow]No benchmark cases found.[/]")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
from rich.table import Table
|
|
71
|
+
|
|
72
|
+
table = Table(title=f"Benchmark Cases ({len(cases)})")
|
|
73
|
+
table.add_column("ID", style="cyan")
|
|
74
|
+
table.add_column("Name")
|
|
75
|
+
table.add_column("Category")
|
|
76
|
+
table.add_column("Min Score", justify="right")
|
|
77
|
+
|
|
78
|
+
for case in cases:
|
|
79
|
+
table.add_row(case.id, case.name, case.category, str(case.expected_min_score))
|
|
80
|
+
|
|
81
|
+
console.print(table)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _handle_run(args: Any) -> None:
|
|
85
|
+
"""Run benchmark cases."""
|
|
86
|
+
suite = BenchmarkSuite()
|
|
87
|
+
|
|
88
|
+
if sys.stdout.encoding and "utf" not in sys.stdout.encoding.lower():
|
|
89
|
+
import io
|
|
90
|
+
|
|
91
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
|
92
|
+
|
|
93
|
+
# Determine which cases to run
|
|
94
|
+
if args.case:
|
|
95
|
+
case_ids = [args.case]
|
|
96
|
+
elif args.category:
|
|
97
|
+
cases = suite.list_cases(category=args.category)
|
|
98
|
+
case_ids = [c.id for c in cases]
|
|
99
|
+
else:
|
|
100
|
+
case_ids = None
|
|
101
|
+
|
|
102
|
+
with console.status("[bold green]Running benchmarks..."):
|
|
103
|
+
result = suite.run(case_ids=case_ids)
|
|
104
|
+
|
|
105
|
+
# Format output
|
|
106
|
+
if args.format == "json":
|
|
107
|
+
# Plain print to avoid Rich line wrapping breaking JSON
|
|
108
|
+
print(format_benchmark_result_json(result))
|
|
109
|
+
elif args.format == "markdown":
|
|
110
|
+
output = format_benchmark_report_markdown(result, output_path=args.output)
|
|
111
|
+
if not args.output:
|
|
112
|
+
console.print(output)
|
|
113
|
+
else:
|
|
114
|
+
console.print(format_benchmark_result(result))
|
|
115
|
+
|
|
116
|
+
if args.format == "markdown" and args.output:
|
|
117
|
+
console.print(f"[green]Report written to {args.output}[/]")
|
|
118
|
+
|
|
119
|
+
# Save baseline
|
|
120
|
+
if args.save:
|
|
121
|
+
path = save_result(result)
|
|
122
|
+
# Print to stderr so it doesn't mix with JSON/markdown stdout output
|
|
123
|
+
import sys as _sys
|
|
124
|
+
|
|
125
|
+
print(f"Baseline saved to {path}", file=_sys.stderr)
|
|
126
|
+
|
|
127
|
+
# Exit code
|
|
128
|
+
if result.failed > 0:
|
|
129
|
+
sys.exit(1)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _handle_compare(args: Any) -> None:
|
|
133
|
+
"""Compare against last saved baseline."""
|
|
134
|
+
from opencontext_core.evaluation.benchmark_suite import compare_results
|
|
135
|
+
|
|
136
|
+
baseline = load_last_result()
|
|
137
|
+
if baseline is None:
|
|
138
|
+
console.print("[yellow]No baseline found. Run `opencontext benchmark run --save` first.[/]")
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
suite = BenchmarkSuite()
|
|
142
|
+
with console.status("[bold green]Running current benchmarks..."):
|
|
143
|
+
current = suite.run()
|
|
144
|
+
|
|
145
|
+
output = compare_results(baseline, current)
|
|
146
|
+
|
|
147
|
+
if args.output:
|
|
148
|
+
Path(args.output).write_text(output, encoding="utf-8")
|
|
149
|
+
console.print(f"[green]Comparison written to {args.output}[/]")
|
|
150
|
+
else:
|
|
151
|
+
console.print(output)
|
|
152
|
+
|
|
153
|
+
# Check for regressions
|
|
154
|
+
regressions = 0
|
|
155
|
+
for r in current.results:
|
|
156
|
+
b_map = {r.case_id: r.score.overall for r in baseline.results}
|
|
157
|
+
if r.case_id in b_map and r.score.overall < b_map[r.case_id] - 2:
|
|
158
|
+
regressions += 1
|
|
159
|
+
|
|
160
|
+
if regressions:
|
|
161
|
+
console.print(f"[red]{regressions} regression(s) detected.[/]")
|
|
162
|
+
sys.exit(1)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Bridges CLI command — scan and display cross-language bridge boundaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from opencontext_core.indexing.bridge_detector import BridgeDetector
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def add_bridges_parser(subparsers: Any) -> None:
|
|
18
|
+
"""Add bridges command parser."""
|
|
19
|
+
bridges_parser = subparsers.add_parser(
|
|
20
|
+
"bridges",
|
|
21
|
+
help="Detect cross-language call boundaries in your project.",
|
|
22
|
+
)
|
|
23
|
+
bridges_sub = bridges_parser.add_subparsers(dest="bridges_command")
|
|
24
|
+
|
|
25
|
+
scan_parser = bridges_sub.add_parser("scan", help="Scan project for cross-language bridges.")
|
|
26
|
+
scan_parser.add_argument("root", nargs="?", default=".", help="Project root to scan.")
|
|
27
|
+
scan_parser.add_argument(
|
|
28
|
+
"--type",
|
|
29
|
+
default=None,
|
|
30
|
+
choices=["HTTP", "GRPC", "CLI_SUBPROCESS", "IPC"],
|
|
31
|
+
help="Filter by bridge type.",
|
|
32
|
+
)
|
|
33
|
+
scan_parser.add_argument(
|
|
34
|
+
"--min-confidence",
|
|
35
|
+
type=float,
|
|
36
|
+
default=0.0,
|
|
37
|
+
help="Minimum confidence threshold (0.0-1.0).",
|
|
38
|
+
)
|
|
39
|
+
scan_parser.add_argument(
|
|
40
|
+
"--json",
|
|
41
|
+
action="store_true",
|
|
42
|
+
dest="json",
|
|
43
|
+
help="Output as JSON.",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
show_parser = bridges_sub.add_parser("show", help="Show bridges for a specific symbol or file.")
|
|
47
|
+
show_parser.add_argument("symbol", help="Symbol name or file path fragment to filter by.")
|
|
48
|
+
show_parser.add_argument("root", nargs="?", default=".", help="Project root to scan.")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def handle_bridges(args: Any) -> None:
|
|
52
|
+
"""Handle bridges commands."""
|
|
53
|
+
command = getattr(args, "bridges_command", None)
|
|
54
|
+
root = getattr(args, "root", ".")
|
|
55
|
+
|
|
56
|
+
if command is None:
|
|
57
|
+
import subprocess
|
|
58
|
+
|
|
59
|
+
subprocess.run(["opencontext", "bridges", "--help"])
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
if command == "scan":
|
|
63
|
+
_handle_scan(
|
|
64
|
+
root,
|
|
65
|
+
bridge_type=getattr(args, "type", None),
|
|
66
|
+
min_confidence=getattr(args, "min_confidence", 0.0),
|
|
67
|
+
output_json=getattr(args, "json", False),
|
|
68
|
+
)
|
|
69
|
+
elif command == "show":
|
|
70
|
+
_handle_show(root, symbol=args.symbol)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _handle_scan(
|
|
74
|
+
root: str,
|
|
75
|
+
bridge_type: str | None = None,
|
|
76
|
+
min_confidence: float = 0.0,
|
|
77
|
+
output_json: bool = False,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Scan and display all detected bridges."""
|
|
80
|
+
with console.status("[bold green]Scanning for cross-language bridges..."):
|
|
81
|
+
detector = BridgeDetector()
|
|
82
|
+
bridges = detector.scan(root)
|
|
83
|
+
|
|
84
|
+
if bridge_type:
|
|
85
|
+
bridges = [b for b in bridges if b.bridge_type == bridge_type]
|
|
86
|
+
if min_confidence > 0:
|
|
87
|
+
bridges = [b for b in bridges if b.confidence >= min_confidence]
|
|
88
|
+
|
|
89
|
+
if not bridges:
|
|
90
|
+
console.print("[dim]No cross-language bridges detected.[/]")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
if output_json:
|
|
94
|
+
print(json.dumps([dataclasses.asdict(b) for b in bridges], indent=2))
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
table = Table(title=f"Cross-Language Bridges ({len(bridges)} found)")
|
|
98
|
+
table.add_column("File", style="cyan", max_width=40)
|
|
99
|
+
table.add_column("Line", justify="right")
|
|
100
|
+
table.add_column("Type", style="yellow")
|
|
101
|
+
table.add_column("Confidence", justify="right")
|
|
102
|
+
table.add_column("Target Hint", max_width=40)
|
|
103
|
+
|
|
104
|
+
for b in bridges:
|
|
105
|
+
conf_style = (
|
|
106
|
+
"green" if b.confidence >= 0.85 else ("yellow" if b.confidence >= 0.7 else "dim")
|
|
107
|
+
)
|
|
108
|
+
table.add_row(
|
|
109
|
+
b.source_file,
|
|
110
|
+
str(b.line),
|
|
111
|
+
b.bridge_type,
|
|
112
|
+
f"[{conf_style}]{b.confidence:.0%}[/]",
|
|
113
|
+
b.target_hint,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
console.print(table)
|
|
117
|
+
|
|
118
|
+
type_counts: dict[str, int] = {}
|
|
119
|
+
for b in bridges:
|
|
120
|
+
type_counts[b.bridge_type] = type_counts.get(b.bridge_type, 0) + 1
|
|
121
|
+
summary = " | ".join(f"{t}: {c}" for t, c in sorted(type_counts.items()))
|
|
122
|
+
console.print(f"[dim]By type: {summary}[/]")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _handle_show(root: str, symbol: str) -> None:
|
|
126
|
+
"""Show bridges filtered by symbol or file path fragment."""
|
|
127
|
+
detector = BridgeDetector()
|
|
128
|
+
bridges = detector.scan(root)
|
|
129
|
+
sym = symbol.lower()
|
|
130
|
+
filtered = [
|
|
131
|
+
b
|
|
132
|
+
for b in bridges
|
|
133
|
+
if sym in b.source_file.lower()
|
|
134
|
+
or sym in b.bridge_type.lower()
|
|
135
|
+
or sym in b.target_hint.lower()
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
if not filtered:
|
|
139
|
+
console.print(f"[dim]No bridges found matching '{symbol}'.[/]")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
table = Table(title=f"Bridges matching '{symbol}'")
|
|
143
|
+
table.add_column("File", style="cyan")
|
|
144
|
+
table.add_column("Line", justify="right")
|
|
145
|
+
table.add_column("Type", style="yellow")
|
|
146
|
+
table.add_column("Target Hint")
|
|
147
|
+
|
|
148
|
+
for b in filtered:
|
|
149
|
+
table.add_row(b.source_file, str(b.line), b.bridge_type, b.target_hint)
|
|
150
|
+
|
|
151
|
+
console.print(table)
|
|
@@ -53,7 +53,9 @@ jobs:
|
|
|
53
53
|
|
|
54
54
|
def add_ci_check_parser(subparsers: Any) -> None:
|
|
55
55
|
"""Add ci-check command parsers."""
|
|
56
|
-
|
|
56
|
+
import argparse
|
|
57
|
+
|
|
58
|
+
check_parser = subparsers.add_parser("ci-check", help=argparse.SUPPRESS)
|
|
57
59
|
check_sub = check_parser.add_subparsers(dest="ci_check_command", required=True)
|
|
58
60
|
check_init = check_sub.add_parser(
|
|
59
61
|
"init", help="Initialize checks directory and ContextBench workflow."
|
|
@@ -94,6 +94,8 @@ def handle_config(args: Any) -> None:
|
|
|
94
94
|
|
|
95
95
|
run_wizard_menu()
|
|
96
96
|
else:
|
|
97
|
+
from opencontext_core.wizard import run_wizard
|
|
98
|
+
|
|
97
99
|
run_wizard(non_interactive=True)
|
|
98
100
|
elif command == "show":
|
|
99
101
|
show_config()
|
|
@@ -237,6 +239,12 @@ def _config_get(key: str) -> None:
|
|
|
237
239
|
print(f"{key} = {value}")
|
|
238
240
|
else:
|
|
239
241
|
print(f"Unknown key: {key}")
|
|
242
|
+
# Suggest the closest key (replace dots with underscores for display)
|
|
243
|
+
candidates = sorted(CONFIG_PATHS.keys())
|
|
244
|
+
key_norm = key.lower().replace(".", "_")
|
|
245
|
+
suggestions = [c for c in candidates if key_norm in c.lower() or c.lower() in key_norm]
|
|
246
|
+
if suggestions:
|
|
247
|
+
print(f"Hint: did you mean {suggestions[0]!r}?")
|
|
240
248
|
print(f"Available paths ({len(CONFIG_PATHS)}):")
|
|
241
249
|
for path, (typ, desc) in sorted(CONFIG_PATHS.items()):
|
|
242
250
|
print(f" {path} ({typ.__name__}) {desc}")
|
|
@@ -302,23 +310,18 @@ def _config_cleanup(keep_days: int) -> None:
|
|
|
302
310
|
except (ValueError, OSError):
|
|
303
311
|
continue
|
|
304
312
|
|
|
305
|
-
# Rebuild index
|
|
306
|
-
[b for b in backups if b.id not in [r.id for r in ConfigBackupManager.list_backups()]]
|
|
307
|
-
# Actually let's rebuild from disk
|
|
313
|
+
# Rebuild index from disk — removes stale index entries for deleted dirs
|
|
308
314
|
index = []
|
|
309
315
|
for entry_dir in sorted(ConfigBackupManager.BACKUP_DIR.iterdir()):
|
|
310
316
|
if entry_dir.is_dir() and entry_dir.name.startswith("backup-"):
|
|
311
317
|
try:
|
|
312
|
-
|
|
313
|
-
desc = "auto-pre-change"
|
|
314
|
-
files =
|
|
315
|
-
for f in entry_dir.iterdir():
|
|
316
|
-
if f.is_file():
|
|
317
|
-
files.append(f.name)
|
|
318
|
+
ts_str = entry_dir.name.replace("backup-", "")
|
|
319
|
+
desc = "auto-pre-change"
|
|
320
|
+
files = sorted(f.name for f in entry_dir.iterdir() if f.is_file())
|
|
318
321
|
index.append(
|
|
319
322
|
{
|
|
320
323
|
"id": entry_dir.name,
|
|
321
|
-
"timestamp":
|
|
324
|
+
"timestamp": ts_str,
|
|
322
325
|
"description": desc,
|
|
323
326
|
"files": files,
|
|
324
327
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Extension CLI command — search, install, list, and remove workflow extensions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from opencontext_core.workflow.extension_registry import ExtensionRegistry
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def add_extension_parser(subparsers: Any) -> None:
|
|
17
|
+
"""Add extension command parser."""
|
|
18
|
+
ext_parser = subparsers.add_parser(
|
|
19
|
+
"extension",
|
|
20
|
+
help="Manage OpenContext workflow extensions.",
|
|
21
|
+
)
|
|
22
|
+
ext_sub = ext_parser.add_subparsers(dest="extension_command", required=True)
|
|
23
|
+
|
|
24
|
+
search_parser = ext_sub.add_parser("search", help="Search available extensions.")
|
|
25
|
+
search_parser.add_argument("query", nargs="?", default="", help="Search query.")
|
|
26
|
+
search_parser.add_argument(
|
|
27
|
+
"--json",
|
|
28
|
+
action="store_true",
|
|
29
|
+
dest="json",
|
|
30
|
+
help="Output as JSON.",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
install_parser = ext_sub.add_parser("install", help="Install an extension.")
|
|
34
|
+
install_parser.add_argument("name", help="Extension name to install.")
|
|
35
|
+
install_parser.add_argument("--root", default=".", help="Project root.")
|
|
36
|
+
|
|
37
|
+
list_parser = ext_sub.add_parser("list", help="List installed extensions.")
|
|
38
|
+
list_parser.add_argument("--root", default=".", help="Project root.")
|
|
39
|
+
|
|
40
|
+
info_parser = ext_sub.add_parser("info", help="Show details for an available extension.")
|
|
41
|
+
info_parser.add_argument("name", help="Extension name.")
|
|
42
|
+
|
|
43
|
+
remove_parser = ext_sub.add_parser("remove", help="Remove an installed extension.")
|
|
44
|
+
remove_parser.add_argument("name", help="Extension name to remove.")
|
|
45
|
+
remove_parser.add_argument("--root", default=".", help="Project root.")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def handle_extension(args: Any) -> None:
|
|
49
|
+
"""Handle extension commands."""
|
|
50
|
+
command = args.extension_command
|
|
51
|
+
registry = ExtensionRegistry()
|
|
52
|
+
root = getattr(args, "root", ".")
|
|
53
|
+
|
|
54
|
+
if command == "search":
|
|
55
|
+
_handle_search(
|
|
56
|
+
registry, getattr(args, "query", ""), output_json=getattr(args, "json", False)
|
|
57
|
+
)
|
|
58
|
+
elif command == "install":
|
|
59
|
+
_handle_install(registry, args.name, root)
|
|
60
|
+
elif command == "list":
|
|
61
|
+
_handle_list(registry, root)
|
|
62
|
+
elif command == "info":
|
|
63
|
+
_handle_info(registry, args.name)
|
|
64
|
+
elif command == "remove":
|
|
65
|
+
_handle_remove(registry, args.name, root)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _handle_search(registry: ExtensionRegistry, query: str, output_json: bool = False) -> None:
|
|
69
|
+
results = registry.search(query)
|
|
70
|
+
if not results:
|
|
71
|
+
console.print("[yellow]No extensions found.[/]")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
if output_json:
|
|
75
|
+
print(json.dumps(results, indent=2))
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
table = Table(title=f"Extensions ({len(results)} found)")
|
|
79
|
+
table.add_column("Name", style="cyan")
|
|
80
|
+
table.add_column("Version")
|
|
81
|
+
table.add_column("Description")
|
|
82
|
+
table.add_column("Tags")
|
|
83
|
+
for ext in results:
|
|
84
|
+
table.add_row(
|
|
85
|
+
ext.get("name", ""),
|
|
86
|
+
ext.get("version", ""),
|
|
87
|
+
ext.get("description", ""),
|
|
88
|
+
", ".join(ext.get("tags", [])),
|
|
89
|
+
)
|
|
90
|
+
console.print(table)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _handle_install(registry: ExtensionRegistry, name: str, root: str) -> None:
|
|
94
|
+
try:
|
|
95
|
+
path = registry.install(name, root=root)
|
|
96
|
+
console.print(f"[green]✓ Installed extension '{name}' to {path}[/]")
|
|
97
|
+
except ValueError as e:
|
|
98
|
+
console.print(f"[red]Error: {e}[/]")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _handle_list(registry: ExtensionRegistry, root: str) -> None:
|
|
102
|
+
installed = registry.list_installed(root=root)
|
|
103
|
+
if not installed:
|
|
104
|
+
console.print(
|
|
105
|
+
"[dim]No extensions installed. Use 'opencontext extension search' to find some.[/]"
|
|
106
|
+
)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
table = Table(title="Installed Extensions")
|
|
110
|
+
table.add_column("Name", style="cyan")
|
|
111
|
+
table.add_column("Version")
|
|
112
|
+
table.add_column("Author")
|
|
113
|
+
table.add_column("Description")
|
|
114
|
+
for m in installed:
|
|
115
|
+
table.add_row(m.name, m.version, m.author, m.description)
|
|
116
|
+
console.print(table)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _handle_info(registry: ExtensionRegistry, name: str) -> None:
|
|
120
|
+
matches = [e for e in registry.search() if e.get("name") == name]
|
|
121
|
+
if not matches:
|
|
122
|
+
console.print(f"[red]Extension not found: {name}[/]")
|
|
123
|
+
return
|
|
124
|
+
ext = matches[0]
|
|
125
|
+
from rich.panel import Panel
|
|
126
|
+
|
|
127
|
+
content = "\n".join(
|
|
128
|
+
[
|
|
129
|
+
f"[bold]Name:[/] {ext.get('name', '')}",
|
|
130
|
+
f"[bold]Version:[/] {ext.get('version', '')}",
|
|
131
|
+
f"[bold]Author:[/] {ext.get('author', '')}",
|
|
132
|
+
f"[bold]Description:[/] {ext.get('description', '')}",
|
|
133
|
+
f"[bold]Tags:[/] {', '.join(ext.get('tags', []))}",
|
|
134
|
+
f"[bold]Requires:[/] opencontext-core >= {ext.get('requires_version', 'any')}",
|
|
135
|
+
]
|
|
136
|
+
)
|
|
137
|
+
console.print(Panel(content, title=f"Extension: {name}", border_style="cyan"))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _handle_remove(registry: ExtensionRegistry, name: str, root: str) -> None:
|
|
141
|
+
removed = registry.remove(name, root=root)
|
|
142
|
+
if removed:
|
|
143
|
+
console.print(f"[green]✓ Removed extension '{name}'[/]")
|
|
144
|
+
else:
|
|
145
|
+
console.print(f"[yellow]Extension '{name}' not found (already removed?)[/]")
|
|
@@ -11,7 +11,9 @@ from opencontext_core.indexing.git_context import GitContextProvider
|
|
|
11
11
|
|
|
12
12
|
def add_git_parser(subparsers: Any) -> None:
|
|
13
13
|
"""Add git command parsers."""
|
|
14
|
-
|
|
14
|
+
import argparse
|
|
15
|
+
|
|
16
|
+
git_parser = subparsers.add_parser("git", help=argparse.SUPPRESS)
|
|
15
17
|
git_sub = git_parser.add_subparsers(dest="git_command", required=True)
|
|
16
18
|
git_sub.add_parser("status", help="Show git repository stats.")
|
|
17
19
|
git_history = git_sub.add_parser("history", help="Show git history for a file.")
|
|
@@ -72,7 +74,7 @@ def handle_git(args: Any) -> None:
|
|
|
72
74
|
elif command == "recent":
|
|
73
75
|
diffs = provider.get_recent_changes(days=days, max_commits=max_commits)
|
|
74
76
|
if json_output:
|
|
75
|
-
|
|
77
|
+
recent_data: list[dict[str, Any]] = [
|
|
76
78
|
{
|
|
77
79
|
"commit_hash": d.commit_hash,
|
|
78
80
|
"author": d.author,
|
|
@@ -82,7 +84,7 @@ def handle_git(args: Any) -> None:
|
|
|
82
84
|
}
|
|
83
85
|
for d in diffs
|
|
84
86
|
]
|
|
85
|
-
print(json.dumps(
|
|
87
|
+
print(json.dumps(recent_data, indent=2))
|
|
86
88
|
else:
|
|
87
89
|
console.header(f"Recent Changes (last {days} days)")
|
|
88
90
|
console.print(_format_git_recent(diffs))
|
|
@@ -11,7 +11,9 @@ from opencontext_core.dx.console_styles import console
|
|
|
11
11
|
|
|
12
12
|
def add_hints_parser(subparsers: Any) -> None:
|
|
13
13
|
"""Add hints command parsers."""
|
|
14
|
-
|
|
14
|
+
import argparse
|
|
15
|
+
|
|
16
|
+
hints_parser = subparsers.add_parser("hints", help=argparse.SUPPRESS)
|
|
15
17
|
hints_sub = hints_parser.add_subparsers(dest="hints_command", required=True)
|
|
16
18
|
hints_sub.add_parser("init", help="Initialize .opencontexthints file.")
|
|
17
19
|
hints_sub.add_parser("show", help="Show combined hints.")
|
|
@@ -45,8 +47,8 @@ def handle_hints(args: Any) -> None:
|
|
|
45
47
|
console.warning("No hints found. Run 'opencontext hints init' to create them.")
|
|
46
48
|
elif command == "validate":
|
|
47
49
|
files = manager.discover_hints()
|
|
48
|
-
valid = []
|
|
49
|
-
invalid = []
|
|
50
|
+
valid: list[str] = []
|
|
51
|
+
invalid: list[str] = []
|
|
50
52
|
for f in files:
|
|
51
53
|
parsed = manager.parse_hints_file(f)
|
|
52
54
|
if parsed:
|
|
@@ -60,8 +62,8 @@ def handle_hints(args: Any) -> None:
|
|
|
60
62
|
console.success(f"Valid: {len(valid)}")
|
|
61
63
|
if invalid:
|
|
62
64
|
console.error(f"Invalid: {len(invalid)}")
|
|
63
|
-
for
|
|
64
|
-
console.print(f" [dim]✗ {
|
|
65
|
+
for item in invalid:
|
|
66
|
+
console.print(f" [dim]✗ {item}[/]")
|
|
65
67
|
console.info(f"Total files checked: {len(files)}")
|
|
66
68
|
else:
|
|
67
69
|
console.error(f"Unknown hints command: {command}")
|