opencontext-cli 0.2.1b0__tar.gz → 0.3.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.2.1b0 → opencontext_cli-0.3.0}/PKG-INFO +1 -1
- opencontext_cli-0.3.0/opencontext_cli/__main__.py +5 -0
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli/commands/ci_check_cmd.py +77 -1
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli/commands/config_cmd.py +116 -35
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli/commands/plugin_cmd.py +163 -1
- opencontext_cli-0.3.0/opencontext_cli/commands/setup_cmd.py +582 -0
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli/commands/update_cmd.py +2 -2
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli/commands/verify_cmd.py +2 -2
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli/main.py +852 -735
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli.egg-info/PKG-INFO +1 -1
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli.egg-info/SOURCES.txt +1 -0
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/pyproject.toml +1 -1
- opencontext_cli-0.2.1b0/opencontext_cli/commands/setup_cmd.py +0 -346
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/LICENSE +0 -0
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/README.md +0 -0
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli/__init__.py +0 -0
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli/commands/__init__.py +0 -0
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli/commands/git_cmd.py +0 -0
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli/commands/hints_cmd.py +0 -0
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli/commands/kg_cmd.py +0 -0
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli/commands/sync_cmd.py +0 -0
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli.egg-info/dependency_links.txt +0 -0
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli.egg-info/entry_points.txt +0 -0
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli.egg-info/requires.txt +0 -0
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/opencontext_cli.egg-info/top_level.txt +0 -0
- {opencontext_cli-0.2.1b0 → opencontext_cli-0.3.0}/setup.cfg +0 -0
|
@@ -3,17 +3,64 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
from opencontext_core.dx.console_styles import console
|
|
9
10
|
from opencontext_core.quality.ci_checks import CheckRunner
|
|
10
11
|
|
|
12
|
+
CONTEXTBENCH_WORKFLOW = """\
|
|
13
|
+
# OpenContext ContextBench CI
|
|
14
|
+
# Auto-generated by `opencontext ci-check init`
|
|
15
|
+
name: OpenContext ContextBench
|
|
16
|
+
|
|
17
|
+
on:
|
|
18
|
+
push:
|
|
19
|
+
branches: [main, master]
|
|
20
|
+
pull_request:
|
|
21
|
+
branches: [main, master]
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
contextbench:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- name: Install OpenContext
|
|
29
|
+
run: pip install opencontext-cli
|
|
30
|
+
- name: Initialize checks
|
|
31
|
+
run: opencontext ci-check init
|
|
32
|
+
- name: Run ContextBench checks
|
|
33
|
+
run: opencontext ci-check run --json > contextbench-report.json
|
|
34
|
+
- name: Upload report
|
|
35
|
+
uses: actions/upload-artifact@v4
|
|
36
|
+
with:
|
|
37
|
+
name: contextbench-report
|
|
38
|
+
path: contextbench-report.json
|
|
39
|
+
- name: Fail on errors
|
|
40
|
+
run: |
|
|
41
|
+
python3 -c "
|
|
42
|
+
import json
|
|
43
|
+
with open('contextbench-report.json') as f:
|
|
44
|
+
report = json.load(f)
|
|
45
|
+
summary = report.get('summary', {})
|
|
46
|
+
if summary.get('failed', 0) > 0:
|
|
47
|
+
print('❌ ContextBench checks failed')
|
|
48
|
+
exit(1)
|
|
49
|
+
print('✅ All ContextBench checks passed')
|
|
50
|
+
"
|
|
51
|
+
"""
|
|
52
|
+
|
|
11
53
|
|
|
12
54
|
def add_ci_check_parser(subparsers: Any) -> None:
|
|
13
55
|
"""Add ci-check command parsers."""
|
|
14
56
|
check_parser = subparsers.add_parser("ci-check", help="CI check management.")
|
|
15
57
|
check_sub = check_parser.add_subparsers(dest="ci_check_command", required=True)
|
|
16
|
-
check_sub.add_parser(
|
|
58
|
+
check_init = check_sub.add_parser(
|
|
59
|
+
"init", help="Initialize checks directory and ContextBench workflow."
|
|
60
|
+
)
|
|
61
|
+
check_init.add_argument(
|
|
62
|
+
"--no-workflow", action="store_true", help="Skip GitHub Actions workflow generation."
|
|
63
|
+
)
|
|
17
64
|
check_sub.add_parser("list", help="List discovered checks.")
|
|
18
65
|
check_run = check_sub.add_parser("run", help="Run all checks.")
|
|
19
66
|
check_run.add_argument("--file", help="Run on specific file only.")
|
|
@@ -21,6 +68,10 @@ def add_ci_check_parser(subparsers: Any) -> None:
|
|
|
21
68
|
check_create = check_sub.add_parser("create", help="Create a new check template.")
|
|
22
69
|
check_create.add_argument("name", help="Check name.")
|
|
23
70
|
check_create.add_argument("--description", default="", help="Check description.")
|
|
71
|
+
check_gh = check_sub.add_parser(
|
|
72
|
+
"github-actions", help="Generate ContextBench GitHub Actions workflow."
|
|
73
|
+
)
|
|
74
|
+
check_gh.add_argument("--force", action="store_true", help="Overwrite existing workflow file.")
|
|
24
75
|
|
|
25
76
|
|
|
26
77
|
def handle_ci_check(args: Any) -> None:
|
|
@@ -35,6 +86,18 @@ def handle_ci_check(args: Any) -> None:
|
|
|
35
86
|
if command == "init":
|
|
36
87
|
path = runner.init_checks_directory()
|
|
37
88
|
console.success(f"Initialized checks directory: {path}")
|
|
89
|
+
skip_workflow = getattr(args, "no_workflow", False)
|
|
90
|
+
if not skip_workflow:
|
|
91
|
+
workflow_path = _generate_contextbench_workflow()
|
|
92
|
+
console.success(f"Generated ContextBench workflow: {workflow_path}")
|
|
93
|
+
elif command == "github-actions":
|
|
94
|
+
force = getattr(args, "force", False)
|
|
95
|
+
workflow_path = Path(".github/workflows/opencontext-contextbench.yml")
|
|
96
|
+
if workflow_path.exists() and not force:
|
|
97
|
+
console.warning(f"Workflow already exists: {workflow_path}. Use --force to overwrite.")
|
|
98
|
+
return
|
|
99
|
+
_write_contextbench_workflow(workflow_path)
|
|
100
|
+
console.success(f"Generated ContextBench workflow: {workflow_path}")
|
|
38
101
|
elif command == "list":
|
|
39
102
|
checks = runner.discover_checks()
|
|
40
103
|
if json_output:
|
|
@@ -113,3 +176,16 @@ def _display_check_report(report: dict[str, Any]) -> None:
|
|
|
113
176
|
console.print(f" [dim]File: {r['file']}:{r.get('line', 'N/A')}[/]")
|
|
114
177
|
if r.get("suggestion"):
|
|
115
178
|
console.print(f" [dim]Suggestion: {r['suggestion']}[/]")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _generate_contextbench_workflow() -> Path:
|
|
182
|
+
"""Generate the ContextBench GitHub Actions workflow as part of init."""
|
|
183
|
+
workflow_path = Path(".github/workflows/opencontext-contextbench.yml")
|
|
184
|
+
_write_contextbench_workflow(workflow_path)
|
|
185
|
+
return workflow_path
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _write_contextbench_workflow(workflow_path: Path) -> None:
|
|
189
|
+
"""Write the ContextBench workflow file."""
|
|
190
|
+
workflow_path.parent.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
workflow_path.write_text(CONTEXTBENCH_WORKFLOW)
|
|
@@ -79,7 +79,13 @@ def handle_config(args: Any) -> None:
|
|
|
79
79
|
command = args.config_command
|
|
80
80
|
|
|
81
81
|
if command == "wizard":
|
|
82
|
-
|
|
82
|
+
use_tui = not getattr(args, "non_interactive", False)
|
|
83
|
+
if use_tui:
|
|
84
|
+
from opencontext_core.wizard import run_wizard_menu
|
|
85
|
+
|
|
86
|
+
run_wizard_menu()
|
|
87
|
+
else:
|
|
88
|
+
run_wizard(non_interactive=True)
|
|
83
89
|
elif command == "show":
|
|
84
90
|
show_config()
|
|
85
91
|
elif command == "reset":
|
|
@@ -100,56 +106,131 @@ def handle_config(args: Any) -> None:
|
|
|
100
106
|
_config_cleanup(args.keep_days)
|
|
101
107
|
|
|
102
108
|
|
|
109
|
+
# ── Dot-notation config paths ──────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
# Schema of configurable paths: "path" -> (type, description)
|
|
112
|
+
CONFIG_PATHS: dict[str, tuple[type, str]] = {
|
|
113
|
+
# Flat keys
|
|
114
|
+
"security_mode": (str, "Security mode: private_project, enterprise, or air-gapped"),
|
|
115
|
+
"default_token_budget": (int, "Default token budget per operation"),
|
|
116
|
+
"max_input_tokens": (int, "Maximum input tokens"),
|
|
117
|
+
"reserve_output_tokens": (int, "Reserved output tokens"),
|
|
118
|
+
"check_updates": (bool, "Check for updates automatically"),
|
|
119
|
+
"auto_optimize": (bool, "Auto-optimize token budgets based on usage"),
|
|
120
|
+
"first_run": (bool, "Whether this is the first run"),
|
|
121
|
+
"default_provider": (str, "Default LLM provider"),
|
|
122
|
+
"default_model": (str, "Default LLM model"),
|
|
123
|
+
# Nested: features.*
|
|
124
|
+
"features.knowledge_graph": (bool, "Knowledge Graph (code indexing & search)"),
|
|
125
|
+
"features.call_graph": (bool, "Call Graph (function call analysis)"),
|
|
126
|
+
"features.learning_system": (bool, "Learning System (auto-optimize)"),
|
|
127
|
+
"features.governance": (bool, "Governance (audit trails & policies)"),
|
|
128
|
+
"features.mcp_server": (bool, "MCP Server (agent integration)"),
|
|
129
|
+
"features.git_integration": (bool, "Git Integration"),
|
|
130
|
+
"features.embeddings": (bool, "Embeddings (semantic search)"),
|
|
131
|
+
"features.semantic_search": (bool, "Semantic Search"),
|
|
132
|
+
# Nested: sdd.*
|
|
133
|
+
"sdd.tdd_mode": (str, "TDD mode: ask, strict, or off"),
|
|
134
|
+
"sdd.sdd_model_profile": (str, "SDD model profile: default, cheap, hybrid, premium"),
|
|
135
|
+
"sdd.orchestrator_profile": (
|
|
136
|
+
str,
|
|
137
|
+
"Orchestrator profile: solo-compact, multi-phase, subagent-native",
|
|
138
|
+
),
|
|
139
|
+
# Nested: agents.*
|
|
140
|
+
"agents.default_client": (str, "Default agent client"),
|
|
141
|
+
"agents.active_clients": (list, "Active agent clients (comma-separated)"),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _resolve_config_path(prefs: Any, dotted: str) -> tuple[Any, str] | None:
|
|
146
|
+
"""Resolve a dotted path to (parent_object, attr_name) or None if invalid.
|
|
147
|
+
|
|
148
|
+
Example: "features.knowledge_graph" -> (prefs.features, "knowledge_graph")
|
|
149
|
+
"""
|
|
150
|
+
parts = dotted.split(".")
|
|
151
|
+
obj = prefs
|
|
152
|
+
for _i, part in enumerate(parts[:-1]):
|
|
153
|
+
if hasattr(obj, part):
|
|
154
|
+
obj = getattr(obj, part)
|
|
155
|
+
else:
|
|
156
|
+
return None
|
|
157
|
+
return (obj, parts[-1])
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _get_all_config_paths() -> list[str]:
|
|
161
|
+
"""Return all available config paths sorted."""
|
|
162
|
+
return sorted(CONFIG_PATHS.keys())
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _coerce_value(value: str, target_type: type) -> object:
|
|
166
|
+
"""Coerce a string value to the target type."""
|
|
167
|
+
if target_type is bool:
|
|
168
|
+
return value.lower() in ("true", "1", "yes", "on")
|
|
169
|
+
elif target_type is int:
|
|
170
|
+
return int(value)
|
|
171
|
+
elif target_type is list:
|
|
172
|
+
import json
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
parsed = json.loads(value)
|
|
176
|
+
if isinstance(parsed, list):
|
|
177
|
+
return parsed
|
|
178
|
+
except (json.JSONDecodeError, TypeError):
|
|
179
|
+
pass
|
|
180
|
+
# Fallback: comma-separated
|
|
181
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
|
182
|
+
else:
|
|
183
|
+
return value
|
|
184
|
+
|
|
185
|
+
|
|
103
186
|
def _config_set(key: str, value: str) -> None:
|
|
104
|
-
"""Set a config value
|
|
187
|
+
"""Set a config value using dot notation."""
|
|
105
188
|
|
|
106
189
|
store = UserConfigStore()
|
|
107
190
|
prefs = store.load()
|
|
108
191
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
else:
|
|
125
|
-
parsed = value
|
|
126
|
-
setattr(prefs, attr_name, parsed)
|
|
127
|
-
store.save(prefs)
|
|
128
|
-
print(f"Set {key} = {parsed}")
|
|
192
|
+
if key in CONFIG_PATHS:
|
|
193
|
+
_target_type, _description = CONFIG_PATHS[key]
|
|
194
|
+
resolved = _resolve_config_path(prefs, key)
|
|
195
|
+
if resolved is None:
|
|
196
|
+
print(f"Error: Cannot resolve path '{key}'")
|
|
197
|
+
return
|
|
198
|
+
parent, attr = resolved
|
|
199
|
+
try:
|
|
200
|
+
parsed = _coerce_value(value, _target_type)
|
|
201
|
+
setattr(parent, attr, parsed)
|
|
202
|
+
store.save(prefs)
|
|
203
|
+
print(f"Set {key} = {parsed}")
|
|
204
|
+
except (ValueError, TypeError) as exc:
|
|
205
|
+
print(f"Error: Cannot set '{key}' to '{value}': {exc}")
|
|
206
|
+
print(f"Expected type: {_target_type.__name__}")
|
|
129
207
|
else:
|
|
130
208
|
print(f"Unknown key: {key}")
|
|
131
|
-
print(f"Available
|
|
209
|
+
print(f"Available paths ({len(CONFIG_PATHS)}):")
|
|
210
|
+
for path, (typ, desc) in sorted(CONFIG_PATHS.items()):
|
|
211
|
+
print(f" {path} ({typ.__name__}) {desc}")
|
|
132
212
|
|
|
133
213
|
|
|
134
214
|
def _config_get(key: str) -> None:
|
|
135
|
-
"""Get a config value by key."""
|
|
215
|
+
"""Get a config value by dot-notation key."""
|
|
136
216
|
|
|
137
217
|
store = UserConfigStore()
|
|
138
218
|
prefs = store.load()
|
|
139
219
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if key in key_map:
|
|
150
|
-
print(f"{key} = {key_map[key]}")
|
|
220
|
+
if key in CONFIG_PATHS:
|
|
221
|
+
_target_type, _description = CONFIG_PATHS[key]
|
|
222
|
+
resolved = _resolve_config_path(prefs, key)
|
|
223
|
+
if resolved is None:
|
|
224
|
+
print(f"Error: Cannot resolve path '{key}'")
|
|
225
|
+
return
|
|
226
|
+
parent, attr = resolved
|
|
227
|
+
value = getattr(parent, attr, "<not set>")
|
|
228
|
+
print(f"{key} = {value}")
|
|
151
229
|
else:
|
|
152
230
|
print(f"Unknown key: {key}")
|
|
231
|
+
print(f"Available paths ({len(CONFIG_PATHS)}):")
|
|
232
|
+
for path, (typ, desc) in sorted(CONFIG_PATHS.items()):
|
|
233
|
+
print(f" {path} ({typ.__name__}) {desc}")
|
|
153
234
|
|
|
154
235
|
|
|
155
236
|
def _config_backup() -> None:
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Usage:
|
|
4
4
|
opencontext plugin list List installed plugins
|
|
5
5
|
opencontext plugin search [query] Search remote registry
|
|
6
|
+
opencontext plugin init <name> Scaffold a new plugin
|
|
6
7
|
opencontext plugin install <name> Install from registry
|
|
7
8
|
opencontext plugin install <name> --github owner/repo
|
|
8
9
|
opencontext plugin install <name> --url <url>
|
|
@@ -16,6 +17,7 @@ Usage:
|
|
|
16
17
|
from __future__ import annotations
|
|
17
18
|
|
|
18
19
|
import json
|
|
20
|
+
from pathlib import Path
|
|
19
21
|
from typing import Any
|
|
20
22
|
|
|
21
23
|
from opencontext_core.plugin_system import (
|
|
@@ -44,6 +46,18 @@ def add_plugin_parser(subparsers: Any) -> None:
|
|
|
44
46
|
"--refresh", action="store_true", help="Force refresh registry cache."
|
|
45
47
|
)
|
|
46
48
|
|
|
49
|
+
# Init
|
|
50
|
+
init_parser = plugin_sub.add_parser("init", help="Scaffold a new plugin.")
|
|
51
|
+
init_parser.add_argument("name", help="Plugin name (alphanumeric + hyphens).")
|
|
52
|
+
init_parser.add_argument("--description", default="", help="Short plugin description.")
|
|
53
|
+
init_parser.add_argument("--author", default="", help="Plugin author name.")
|
|
54
|
+
init_parser.add_argument(
|
|
55
|
+
"--template",
|
|
56
|
+
choices=["basic", "advanced"],
|
|
57
|
+
default="basic",
|
|
58
|
+
help="Scaffold template to use (default: basic).",
|
|
59
|
+
)
|
|
60
|
+
|
|
47
61
|
# Install
|
|
48
62
|
install_parser = plugin_sub.add_parser("install", help="Install a plugin.")
|
|
49
63
|
install_parser.add_argument("name", help="Plugin name.")
|
|
@@ -65,6 +79,7 @@ def add_plugin_parser(subparsers: Any) -> None:
|
|
|
65
79
|
# Info
|
|
66
80
|
info_parser = plugin_sub.add_parser("info", help="Show plugin details.")
|
|
67
81
|
info_parser.add_argument("name", help="Plugin name.")
|
|
82
|
+
info_parser.add_argument("--json", action="store_true", help="Output as JSON.")
|
|
68
83
|
|
|
69
84
|
# Enable/Disable
|
|
70
85
|
enable_parser = plugin_sub.add_parser("enable", help="Enable a plugin.")
|
|
@@ -83,6 +98,8 @@ def handle_plugin(args: Any) -> None:
|
|
|
83
98
|
_plugin_list(args)
|
|
84
99
|
elif command == "search":
|
|
85
100
|
_plugin_search(args)
|
|
101
|
+
elif command == "init":
|
|
102
|
+
_plugin_init(args)
|
|
86
103
|
elif command == "install":
|
|
87
104
|
_plugin_install(args)
|
|
88
105
|
elif command == "remove":
|
|
@@ -180,6 +197,107 @@ def _plugin_search(args: Any) -> None:
|
|
|
180
197
|
print(" Details: opencontext plugin info <name>")
|
|
181
198
|
|
|
182
199
|
|
|
200
|
+
def _plugin_init(args: Any) -> None:
|
|
201
|
+
"""Scaffold a new plugin directory."""
|
|
202
|
+
|
|
203
|
+
name = args.name.strip()
|
|
204
|
+
if not name.replace("-", "").replace("_", "").isalnum():
|
|
205
|
+
print(f"\n ✗ Invalid plugin name: '{name}'. Use alphanumeric, hyphens, or underscores.\n")
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
plugin_dir = Path.cwd() / name
|
|
209
|
+
if plugin_dir.exists():
|
|
210
|
+
print(f"\n ✗ Directory '{name}' already exists.\n")
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
description = args.description or f"Plugin '{name}'"
|
|
214
|
+
author = args.author or ""
|
|
215
|
+
class_name = "".join(part.capitalize() for part in name.replace("-", "_").split("_"))
|
|
216
|
+
if class_name.endswith("Plugin"):
|
|
217
|
+
base_name = class_name
|
|
218
|
+
else:
|
|
219
|
+
base_name = f"{class_name}Plugin"
|
|
220
|
+
|
|
221
|
+
plugin_dir.mkdir(parents=True, exist_ok=True)
|
|
222
|
+
|
|
223
|
+
# --- plugin.yaml ---
|
|
224
|
+
yaml_content = (
|
|
225
|
+
f"name: {name}\n"
|
|
226
|
+
f"version: 0.1.0\n"
|
|
227
|
+
f"description: {description}\n"
|
|
228
|
+
f"author: {author}\n"
|
|
229
|
+
f"entry_point: plugin.py\n"
|
|
230
|
+
f"hooks: []\n"
|
|
231
|
+
)
|
|
232
|
+
(plugin_dir / "plugin.yaml").write_text(yaml_content, encoding="utf-8")
|
|
233
|
+
print(f" ✓ Created {name}/plugin.yaml")
|
|
234
|
+
|
|
235
|
+
# --- plugin.py ---
|
|
236
|
+
if args.template == "advanced":
|
|
237
|
+
plugin_py = (
|
|
238
|
+
f'"""Advanced {name} plugin."""\n\n'
|
|
239
|
+
f"from __future__ import annotations\n\n"
|
|
240
|
+
f"from typing import Any\n\n\n"
|
|
241
|
+
f"class {base_name}:\n"
|
|
242
|
+
f' """{name} plugin."""\n\n'
|
|
243
|
+
f" @property\n"
|
|
244
|
+
f" def name(self) -> str:\n"
|
|
245
|
+
f' return "{name}"\n\n'
|
|
246
|
+
f" @property\n"
|
|
247
|
+
f" def version(self) -> str:\n"
|
|
248
|
+
f' return "0.1.0"\n\n'
|
|
249
|
+
f" @property\n"
|
|
250
|
+
f" def description(self) -> str:\n"
|
|
251
|
+
f' return "{description}"\n\n'
|
|
252
|
+
f" def initialize(self, context: dict[str, Any]) -> None:\n"
|
|
253
|
+
f' """Called when plugin is loaded."""\n'
|
|
254
|
+
f" pass\n\n"
|
|
255
|
+
f" def shutdown(self) -> None:\n"
|
|
256
|
+
f' """Called when plugin is unloaded."""\n'
|
|
257
|
+
f" pass\n\n"
|
|
258
|
+
f" def register_commands(self, registry: Any) -> None:\n"
|
|
259
|
+
f' """Register CLI commands."""\n'
|
|
260
|
+
f" pass\n\n"
|
|
261
|
+
f" def register_hooks(self, registry: Any) -> None:\n"
|
|
262
|
+
f' """Register hooks."""\n'
|
|
263
|
+
f' registry.register_hook("post_execute", self.on_post_execute)\n\n'
|
|
264
|
+
f" def on_post_execute(self, result: Any) -> None:\n"
|
|
265
|
+
f" pass\n"
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
plugin_py = (
|
|
269
|
+
f'"""{name} plugin."""\n\n\n'
|
|
270
|
+
f"class {base_name}:\n"
|
|
271
|
+
f" @property\n"
|
|
272
|
+
f" def name(self):\n"
|
|
273
|
+
f' return "{name}"\n'
|
|
274
|
+
f"\n"
|
|
275
|
+
f" @property\n"
|
|
276
|
+
f" def version(self):\n"
|
|
277
|
+
f' return "0.1.0"\n'
|
|
278
|
+
f"\n"
|
|
279
|
+
f" @property\n"
|
|
280
|
+
f" def description(self):\n"
|
|
281
|
+
f' return "{description}"\n'
|
|
282
|
+
)
|
|
283
|
+
(plugin_dir / "plugin.py").write_text(plugin_py, encoding="utf-8")
|
|
284
|
+
print(f" ✓ Created {name}/plugin.py")
|
|
285
|
+
|
|
286
|
+
# --- README.md ---
|
|
287
|
+
readme = (
|
|
288
|
+
f"# {name}\n\n"
|
|
289
|
+
f"{description}\n\n"
|
|
290
|
+
f"## Installation\n\n"
|
|
291
|
+
f"```bash\nopencontext plugin install {name}\n```\n\n"
|
|
292
|
+
f"## Usage\n\n"
|
|
293
|
+
f"Describe how to use this plugin.\n"
|
|
294
|
+
)
|
|
295
|
+
(plugin_dir / "README.md").write_text(readme, encoding="utf-8")
|
|
296
|
+
print(f" ✓ Created {name}/README.md")
|
|
297
|
+
|
|
298
|
+
print(f"\n Plugin '{name}' scaffolded. Edit plugin.py to add your logic.\n")
|
|
299
|
+
|
|
300
|
+
|
|
183
301
|
def _plugin_install(args: Any) -> None:
|
|
184
302
|
"""Install a plugin."""
|
|
185
303
|
|
|
@@ -289,6 +407,46 @@ def _plugin_info(args: Any) -> None:
|
|
|
289
407
|
registry = PluginRegistry()
|
|
290
408
|
info = registry.get_info(args.name)
|
|
291
409
|
|
|
410
|
+
# Check registry for latest version
|
|
411
|
+
latest_version = "unknown"
|
|
412
|
+
try:
|
|
413
|
+
fetcher = RegistryFetcher()
|
|
414
|
+
entry = fetcher.get(args.name)
|
|
415
|
+
if entry and entry.versions:
|
|
416
|
+
latest_version = entry.versions[0].version
|
|
417
|
+
except Exception:
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
if args.json:
|
|
421
|
+
if info is None:
|
|
422
|
+
data = {
|
|
423
|
+
"name": args.name,
|
|
424
|
+
"installed": False,
|
|
425
|
+
"latest": latest_version,
|
|
426
|
+
}
|
|
427
|
+
else:
|
|
428
|
+
data = {
|
|
429
|
+
"name": info.name,
|
|
430
|
+
"installed": True,
|
|
431
|
+
"version": info.version,
|
|
432
|
+
"latest": latest_version,
|
|
433
|
+
"description": info.description,
|
|
434
|
+
"author": info.author,
|
|
435
|
+
"homepage": info.homepage,
|
|
436
|
+
"repository": info.repository,
|
|
437
|
+
"enabled": info.enabled,
|
|
438
|
+
"install_source": info.install_source,
|
|
439
|
+
"source_url": info.source_url,
|
|
440
|
+
"entry_point": info.entry_point,
|
|
441
|
+
"installed_at": info.installed_at,
|
|
442
|
+
"updated_at": info.updated_at,
|
|
443
|
+
"hooks": info.hooks,
|
|
444
|
+
}
|
|
445
|
+
if latest_version != "unknown" and latest_version != info.version:
|
|
446
|
+
data["update_available"] = True
|
|
447
|
+
print(json.dumps(data, indent=2))
|
|
448
|
+
return
|
|
449
|
+
|
|
292
450
|
if info is None:
|
|
293
451
|
# Check registry
|
|
294
452
|
fetcher = RegistryFetcher()
|
|
@@ -311,11 +469,15 @@ def _plugin_info(args: Any) -> None:
|
|
|
311
469
|
print(f"\n {info.name}")
|
|
312
470
|
print(f" {'─' * len(info.name)}")
|
|
313
471
|
print(f" Version: {info.version}")
|
|
472
|
+
if latest_version != "unknown" and latest_version != info.version:
|
|
473
|
+
print(f" Latest: {latest_version} (update available)")
|
|
474
|
+
else:
|
|
475
|
+
print(f" Latest: {latest_version}")
|
|
314
476
|
print(f" Description: {info.description}")
|
|
315
477
|
print(f" Author: {info.author or '—'}")
|
|
316
478
|
print(f" Homepage: {info.homepage or '—'}")
|
|
317
479
|
print(f" Repository: {info.repository or '—'}")
|
|
318
|
-
print(f" Status: {'
|
|
480
|
+
print(f" Status: {'enabled' if info.enabled else 'disabled'}")
|
|
319
481
|
print(f" Source: {info.install_source}")
|
|
320
482
|
print(f" Source URL: {info.source_url or '—'}")
|
|
321
483
|
print(f" Entry point: {info.entry_point}")
|