10xgraph-api 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 10xgraph_api-0.7.0.dist-info/METADATA +618 -0
- 10xgraph_api-0.7.0.dist-info/RECORD +203 -0
- 10xgraph_api-0.7.0.dist-info/WHEEL +5 -0
- 10xgraph_api-0.7.0.dist-info/entry_points.txt +3 -0
- 10xgraph_api-0.7.0.dist-info/licenses/LICENSE +21 -0
- 10xgraph_api-0.7.0.dist-info/top_level.txt +2 -0
- agentflow_cli/__init__.py +69 -0
- tenxgraph_api/__init__.py +23 -0
- tenxgraph_api/cli/__init__.py +6 -0
- tenxgraph_api/cli/capabilities.py +127 -0
- tenxgraph_api/cli/commands/__init__.py +49 -0
- tenxgraph_api/cli/commands/api.py +262 -0
- tenxgraph_api/cli/commands/audit.py +213 -0
- tenxgraph_api/cli/commands/build.py +305 -0
- tenxgraph_api/cli/commands/config.py +71 -0
- tenxgraph_api/cli/commands/demo.py +93 -0
- tenxgraph_api/cli/commands/eval.py +909 -0
- tenxgraph_api/cli/commands/init.py +531 -0
- tenxgraph_api/cli/commands/skills.py +532 -0
- tenxgraph_api/cli/commands/test.py +113 -0
- tenxgraph_api/cli/commands/version.py +47 -0
- tenxgraph_api/cli/config_editor/__init__.py +15 -0
- tenxgraph_api/cli/config_editor/schema.py +566 -0
- tenxgraph_api/cli/config_editor/server.py +240 -0
- tenxgraph_api/cli/config_editor/static/app.css +2 -0
- tenxgraph_api/cli/config_editor/static/app.js +3 -0
- tenxgraph_api/cli/config_editor/static/favicon.svg +11 -0
- tenxgraph_api/cli/config_editor/static/index.html +20 -0
- tenxgraph_api/cli/config_editor/store.py +120 -0
- tenxgraph_api/cli/config_editor/validation.py +377 -0
- tenxgraph_api/cli/constants.py +112 -0
- tenxgraph_api/cli/context.py +46 -0
- tenxgraph_api/cli/core/__init__.py +0 -0
- tenxgraph_api/cli/core/animations.py +550 -0
- tenxgraph_api/cli/core/config.py +279 -0
- tenxgraph_api/cli/core/output.py +625 -0
- tenxgraph_api/cli/core/prompts.py +220 -0
- tenxgraph_api/cli/core/screen.py +287 -0
- tenxgraph_api/cli/core/steps.py +621 -0
- tenxgraph_api/cli/core/theme.py +242 -0
- tenxgraph_api/cli/core/validation.py +251 -0
- tenxgraph_api/cli/exceptions.py +129 -0
- tenxgraph_api/cli/logger.py +84 -0
- tenxgraph_api/cli/main.py +936 -0
- tenxgraph_api/cli/templates/__init__.py +0 -0
- tenxgraph_api/cli/templates/defaults.py +844 -0
- tenxgraph_api/cli/templates/dev/.env.example +1 -0
- tenxgraph_api/cli/templates/dev/10xgraph.json +16 -0
- tenxgraph_api/cli/templates/dev/graph/__init__.py +0 -0
- tenxgraph_api/cli/templates/dev/graph/agent.py +63 -0
- tenxgraph_api/cli/templates/prod/.env.example +75 -0
- tenxgraph_api/cli/templates/prod/.pre-commit-config.yaml +50 -0
- tenxgraph_api/cli/templates/prod/.python-version +1 -0
- tenxgraph_api/cli/templates/prod/10xgraph.json +42 -0
- tenxgraph_api/cli/templates/prod/auth/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/auth/agent_auth.py +46 -0
- tenxgraph_api/cli/templates/prod/evals/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/evals/user_simulator_eval.py +76 -0
- tenxgraph_api/cli/templates/prod/evals/weather_agents_eval.py +53 -0
- tenxgraph_api/cli/templates/prod/graph/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/agent.py +50 -0
- tenxgraph_api/cli/templates/prod/graph/state.py +5 -0
- tenxgraph_api/cli/templates/prod/graph/thread_name_generator.py +7 -0
- tenxgraph_api/cli/templates/prod/graph/tools/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/tools/weather_tool.py +43 -0
- tenxgraph_api/cli/templates/prod/graph/validators/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/validators/lifecyle.py +107 -0
- tenxgraph_api/cli/templates/prod/graph/validators/manager.py +9 -0
- tenxgraph_api/cli/templates/prod/graph/validators/validators.py +21 -0
- tenxgraph_api/cli/templates/prod/pyproject.toml +79 -0
- tenxgraph_api/cli/templates/prod/tests/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/tests/conftest.py +6 -0
- tenxgraph_api/cli/templates/prod/tests/test_agent_eval.py +78 -0
- tenxgraph_api/cli/templates/prod/tests/test_catalog_tools.py +47 -0
- tenxgraph_api/cli/templates/prod/tests/test_graph_nodes.py +57 -0
- tenxgraph_api/cli/templates/skills/10xgraph/SKILL.md +92 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/agents-and-tools.md +389 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-client.md +61 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-configuration.md +75 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-settings-and-middleware.md +89 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/architecture.md +91 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/auth-and-authorization.md +149 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/callbacks-and-command.md +283 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/checkpointing-and-threads.md +63 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/cli-commands.md +73 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-auth-and-errors.md +94 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-messages-invoke-stream.md +115 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-threads-memory-files.md +80 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/context-id-background.md +80 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/dependency-injection.md +58 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/evaluation.md +500 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/id-and-thread-name-generators.md +69 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/media-and-files.md +66 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/memory-and-store.md +70 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/prebuilt-agents-and-tools.md +394 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/production-runtime.md +56 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/providers-and-adapters.md +89 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md +84 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/rate-limiting.md +194 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/realtime.md +172 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/remote-tools.md +55 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/rest-api-and-errors.md +97 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/security-and-validators.md +86 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/state-and-messages.md +68 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/state-graph.md +232 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/stream-emitter.md +144 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/streaming.md +53 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/testing-and-evaluation.md +64 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/unit-testing.md +202 -0
- tenxgraph_api/cli/templates/skills/copilot/10xgraph.instructions.md +108 -0
- tenxgraph_api/py.typed +0 -0
- tenxgraph_api/src/__init__.py +0 -0
- tenxgraph_api/src/app/__init__.py +13 -0
- tenxgraph_api/src/app/core/__init__.py +15 -0
- tenxgraph_api/src/app/core/auth/__init__.py +0 -0
- tenxgraph_api/src/app/core/auth/auth_backend.py +38 -0
- tenxgraph_api/src/app/core/auth/authorization.py +379 -0
- tenxgraph_api/src/app/core/auth/base_auth.py +27 -0
- tenxgraph_api/src/app/core/auth/jwt_auth.py +122 -0
- tenxgraph_api/src/app/core/auth/ownership_resolver.py +170 -0
- tenxgraph_api/src/app/core/auth/permissions.py +354 -0
- tenxgraph_api/src/app/core/auth/request_config.py +130 -0
- tenxgraph_api/src/app/core/auth/route_guard.py +105 -0
- tenxgraph_api/src/app/core/config/__init__.py +0 -0
- tenxgraph_api/src/app/core/config/graph_config.py +532 -0
- tenxgraph_api/src/app/core/config/media_settings.py +72 -0
- tenxgraph_api/src/app/core/config/sentry_config.py +60 -0
- tenxgraph_api/src/app/core/config/settings.py +202 -0
- tenxgraph_api/src/app/core/config/setup_logs.py +65 -0
- tenxgraph_api/src/app/core/config/setup_middleware.py +483 -0
- tenxgraph_api/src/app/core/config/worker_middleware.py +99 -0
- tenxgraph_api/src/app/core/exceptions/__init__.py +17 -0
- tenxgraph_api/src/app/core/exceptions/general_exception.py +58 -0
- tenxgraph_api/src/app/core/exceptions/handle_errors.py +363 -0
- tenxgraph_api/src/app/core/exceptions/resources_exceptions.py +72 -0
- tenxgraph_api/src/app/core/exceptions/user_exception.py +58 -0
- tenxgraph_api/src/app/core/middleware/__init__.py +25 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/__init__.py +15 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/base.py +27 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/factory.py +98 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/keying.py +133 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/memory.py +98 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/middleware.py +90 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/redis.py +115 -0
- tenxgraph_api/src/app/core/middleware/request_limits.py +154 -0
- tenxgraph_api/src/app/core/middleware/security_headers.py +239 -0
- tenxgraph_api/src/app/core/utils/__init__.py +14 -0
- tenxgraph_api/src/app/core/utils/log_sanitizer.py +216 -0
- tenxgraph_api/src/app/loader.py +516 -0
- tenxgraph_api/src/app/main.py +180 -0
- tenxgraph_api/src/app/routers/__init__.py +4 -0
- tenxgraph_api/src/app/routers/ag_ui/__init__.py +10 -0
- tenxgraph_api/src/app/routers/ag_ui/converter.py +195 -0
- tenxgraph_api/src/app/routers/ag_ui/event_mapper.py +353 -0
- tenxgraph_api/src/app/routers/ag_ui/router.py +90 -0
- tenxgraph_api/src/app/routers/ag_ui/service.py +193 -0
- tenxgraph_api/src/app/routers/checkpointer/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/router.py +482 -0
- tenxgraph_api/src/app/routers/checkpointer/schemas/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/schemas/checkpointer_schemas.py +121 -0
- tenxgraph_api/src/app/routers/checkpointer/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/services/checkpointer_service.py +300 -0
- tenxgraph_api/src/app/routers/evals/__init__.py +6 -0
- tenxgraph_api/src/app/routers/evals/router.py +38 -0
- tenxgraph_api/src/app/routers/evals/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/evals/services/eval_report_service.py +354 -0
- tenxgraph_api/src/app/routers/graph/__init__.py +6 -0
- tenxgraph_api/src/app/routers/graph/realtime_guard.py +161 -0
- tenxgraph_api/src/app/routers/graph/router.py +821 -0
- tenxgraph_api/src/app/routers/graph/schemas/__init__.py +1 -0
- tenxgraph_api/src/app/routers/graph/schemas/graph_schemas.py +386 -0
- tenxgraph_api/src/app/routers/graph/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/graph/services/graph_service.py +1219 -0
- tenxgraph_api/src/app/routers/graph/services/multimodal_preprocessor.py +164 -0
- tenxgraph_api/src/app/routers/media/__init__.py +506 -0
- tenxgraph_api/src/app/routers/media/router.py +242 -0
- tenxgraph_api/src/app/routers/media/schemas.py +39 -0
- tenxgraph_api/src/app/routers/ping/__init__.py +4 -0
- tenxgraph_api/src/app/routers/ping/router.py +28 -0
- tenxgraph_api/src/app/routers/setup_router.py +60 -0
- tenxgraph_api/src/app/routers/store/__init__.py +4 -0
- tenxgraph_api/src/app/routers/store/router.py +200 -0
- tenxgraph_api/src/app/routers/store/schemas/__init__.py +28 -0
- tenxgraph_api/src/app/routers/store/schemas/store_schemas.py +200 -0
- tenxgraph_api/src/app/routers/store/services/__init__.py +4 -0
- tenxgraph_api/src/app/routers/store/services/store_service.py +203 -0
- tenxgraph_api/src/app/tasks/__init__.py +0 -0
- tenxgraph_api/src/app/tasks/user_tasks.py +38 -0
- tenxgraph_api/src/app/utils/__init__.py +12 -0
- tenxgraph_api/src/app/utils/callable_helper.py +26 -0
- tenxgraph_api/src/app/utils/media/__init__.py +15 -0
- tenxgraph_api/src/app/utils/media/extractor.py +172 -0
- tenxgraph_api/src/app/utils/media/pipeline.py +83 -0
- tenxgraph_api/src/app/utils/parse_output.py +17 -0
- tenxgraph_api/src/app/utils/response_helper.py +129 -0
- tenxgraph_api/src/app/utils/schemas/__init__.py +11 -0
- tenxgraph_api/src/app/utils/schemas/output_schemas.py +28 -0
- tenxgraph_api/src/app/utils/schemas/user_schemas.py +13 -0
- tenxgraph_api/src/app/utils/snowflake_id_generator.py +90 -0
- tenxgraph_api/src/app/utils/swagger_helper.py +160 -0
- tenxgraph_api/src/app/utils/telemetry_store.py +123 -0
- tenxgraph_api/src/app/utils/thread_name_generator.py +304 -0
- tenxgraph_api/src/app/worker.py +91 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
"""Skills command implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
from tenxgraph_api.cli.commands import BaseCommand
|
|
13
|
+
from tenxgraph_api.cli.constants import CLI_VERSION
|
|
14
|
+
from tenxgraph_api.cli.core.prompts import Choice
|
|
15
|
+
from tenxgraph_api.cli.exceptions import FileOperationError, ValidationError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_MANIFEST_FILENAME = ".10xgraph-skill.json"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class _InstallArtifact:
|
|
23
|
+
"""Describes one file or folder installed for an agent."""
|
|
24
|
+
|
|
25
|
+
kind: Literal["folder", "file"]
|
|
26
|
+
install_relpath: str
|
|
27
|
+
source_relpath: str
|
|
28
|
+
manifest: bool = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class _AgentTarget:
|
|
33
|
+
"""Describes how the bundled skill is materialised for one agent."""
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
artifacts: tuple[_InstallArtifact, ...]
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def kind(self) -> str:
|
|
40
|
+
kinds = {artifact.kind for artifact in self.artifacts}
|
|
41
|
+
if len(kinds) == 1:
|
|
42
|
+
return next(iter(kinds))
|
|
43
|
+
return "file+folder"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Every agent gets the same spec-conformant skill folder (SKILL.md + references/);
|
|
47
|
+
# the SKILL.md uses paths relative to the skill directory, so one copy works in
|
|
48
|
+
# any install location.
|
|
49
|
+
_SKILL_SOURCE = "10xgraph"
|
|
50
|
+
|
|
51
|
+
_TARGETS: tuple[_AgentTarget, ...] = (
|
|
52
|
+
_AgentTarget(
|
|
53
|
+
name="Codex",
|
|
54
|
+
artifacts=(
|
|
55
|
+
_InstallArtifact(
|
|
56
|
+
kind="folder",
|
|
57
|
+
install_relpath=".agents/skills/10xgraph",
|
|
58
|
+
source_relpath=_SKILL_SOURCE,
|
|
59
|
+
manifest=True,
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
_AgentTarget(
|
|
64
|
+
name="Claude",
|
|
65
|
+
artifacts=(
|
|
66
|
+
_InstallArtifact(
|
|
67
|
+
kind="folder",
|
|
68
|
+
install_relpath=".claude/skills/10xgraph",
|
|
69
|
+
source_relpath=_SKILL_SOURCE,
|
|
70
|
+
manifest=True,
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
_AgentTarget(
|
|
75
|
+
name="GitHub",
|
|
76
|
+
artifacts=(
|
|
77
|
+
_InstallArtifact(
|
|
78
|
+
kind="file",
|
|
79
|
+
install_relpath=".github/instructions/10xgraph.instructions.md",
|
|
80
|
+
source_relpath="copilot/10xgraph.instructions.md",
|
|
81
|
+
),
|
|
82
|
+
_InstallArtifact(
|
|
83
|
+
kind="folder",
|
|
84
|
+
install_relpath=".github/skills/10xgraph",
|
|
85
|
+
source_relpath=_SKILL_SOURCE,
|
|
86
|
+
manifest=True,
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
_AGENT_LOOKUP: dict[str, _AgentTarget] = {
|
|
93
|
+
**{t.name.lower(): t for t in _TARGETS},
|
|
94
|
+
"1": _TARGETS[0],
|
|
95
|
+
"2": _TARGETS[1],
|
|
96
|
+
"3": _TARGETS[2],
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class SkillsCommand(BaseCommand):
|
|
101
|
+
"""Install bundled 10xGraph skills, or validate skills against the spec."""
|
|
102
|
+
|
|
103
|
+
def execute(
|
|
104
|
+
self,
|
|
105
|
+
agent: str | None = None,
|
|
106
|
+
path: str = ".",
|
|
107
|
+
force: bool = False,
|
|
108
|
+
all_agents: bool = False,
|
|
109
|
+
list_agents: bool = False,
|
|
110
|
+
validate_paths: list[str] | None = None,
|
|
111
|
+
**kwargs: Any,
|
|
112
|
+
) -> int:
|
|
113
|
+
"""Execute the skills command.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
agent: Target agent name or menu number.
|
|
117
|
+
path: Project directory where the agent skill should be installed.
|
|
118
|
+
force: Overwrite an existing installation.
|
|
119
|
+
all_agents: Install for every supported agent.
|
|
120
|
+
list_agents: Print supported agents and exit.
|
|
121
|
+
validate_paths: Skill directories (or folders of skills) to validate
|
|
122
|
+
against the Agent Skills specification instead of installing.
|
|
123
|
+
**kwargs: Additional arguments.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Exit code.
|
|
127
|
+
"""
|
|
128
|
+
if validate_paths:
|
|
129
|
+
return self._run_validate(validate_paths)
|
|
130
|
+
return self._run_install(
|
|
131
|
+
agent, path, force=force, all_agents=all_agents, list_agents=list_agents
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def _run_validate(self, validate_paths: list[str]) -> int:
|
|
135
|
+
try:
|
|
136
|
+
self.output.command_header(
|
|
137
|
+
"skills",
|
|
138
|
+
"Validate skills against the Agent Skills specification (agentskills.io).",
|
|
139
|
+
color="magenta",
|
|
140
|
+
)
|
|
141
|
+
return self._validate(validate_paths)
|
|
142
|
+
except ValidationError as e:
|
|
143
|
+
return self.handle_error(e)
|
|
144
|
+
|
|
145
|
+
def _run_install(
|
|
146
|
+
self,
|
|
147
|
+
agent: str | None,
|
|
148
|
+
path: str,
|
|
149
|
+
*,
|
|
150
|
+
force: bool,
|
|
151
|
+
all_agents: bool,
|
|
152
|
+
list_agents: bool,
|
|
153
|
+
) -> int:
|
|
154
|
+
try:
|
|
155
|
+
self.output.command_header(
|
|
156
|
+
"skills",
|
|
157
|
+
"Install bundled 10xGraph skills for Codex, Claude, or GitHub Copilot.",
|
|
158
|
+
color="magenta",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if list_agents:
|
|
162
|
+
self._print_agents()
|
|
163
|
+
return 0
|
|
164
|
+
|
|
165
|
+
if all_agents and agent:
|
|
166
|
+
raise ValidationError("--all cannot be combined with --agent.", field="agent")
|
|
167
|
+
|
|
168
|
+
templates_root = self._templates_root()
|
|
169
|
+
project_root = self._safe_project_root(path)
|
|
170
|
+
|
|
171
|
+
if all_agents:
|
|
172
|
+
targets: tuple[_AgentTarget, ...] = _TARGETS
|
|
173
|
+
elif agent:
|
|
174
|
+
targets = (self._normalize_agent(agent),)
|
|
175
|
+
else:
|
|
176
|
+
selected = self._choose_agents(project_root)
|
|
177
|
+
if selected is None:
|
|
178
|
+
self.output.info("Cancelled.", emoji=False)
|
|
179
|
+
return 0
|
|
180
|
+
targets = selected
|
|
181
|
+
|
|
182
|
+
force = force or self._confirm_overwrite(project_root, targets, force=force)
|
|
183
|
+
return self._install_targets(
|
|
184
|
+
templates_root,
|
|
185
|
+
project_root,
|
|
186
|
+
targets,
|
|
187
|
+
force=force,
|
|
188
|
+
# Naming one agent is a request for that agent specifically, so a
|
|
189
|
+
# collision is an error. A set the user did not enumerate — --all,
|
|
190
|
+
# or a multi-select — skips what is already there instead.
|
|
191
|
+
strict=bool(agent),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
except (FileOperationError, ValidationError) as e:
|
|
195
|
+
return self.handle_error(e)
|
|
196
|
+
except OSError as e:
|
|
197
|
+
file_error = FileOperationError(f"Failed to install 10xGraph skills: {e}")
|
|
198
|
+
file_error.__cause__ = e
|
|
199
|
+
return self.handle_error(file_error)
|
|
200
|
+
|
|
201
|
+
# ------------------------------------------------------------------
|
|
202
|
+
# Validation
|
|
203
|
+
# ------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
def _validate(self, paths: list[str]) -> int:
|
|
206
|
+
"""Validate each skill found under *paths*. Returns 1 if any is invalid."""
|
|
207
|
+
try:
|
|
208
|
+
# Not in any released core yet (<= 0.9.2); the ImportError branch covers that.
|
|
209
|
+
from tenxgraph.core.skills import validate_skill # type: ignore[attr-defined]
|
|
210
|
+
from tenxgraph.core.skills.loader import ( # type: ignore[attr-defined]
|
|
211
|
+
iter_skill_dirs,
|
|
212
|
+
)
|
|
213
|
+
except ImportError as exc:
|
|
214
|
+
raise ValidationError(
|
|
215
|
+
"Skill validation needs a 10xgraph release that ships "
|
|
216
|
+
"tenxgraph.core.skills.validate_skill. Upgrade 10xgraph.",
|
|
217
|
+
field="validate",
|
|
218
|
+
) from exc
|
|
219
|
+
|
|
220
|
+
rows: list[list[str]] = []
|
|
221
|
+
invalid = 0
|
|
222
|
+
checked = 0
|
|
223
|
+
for raw_path in paths:
|
|
224
|
+
skill_dirs = iter_skill_dirs(raw_path)
|
|
225
|
+
if not skill_dirs:
|
|
226
|
+
self.output.error(
|
|
227
|
+
f"No skill found at {raw_path}: expected a SKILL.md in it or in its "
|
|
228
|
+
"subdirectories."
|
|
229
|
+
)
|
|
230
|
+
invalid += 1
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
for skill_dir in skill_dirs:
|
|
234
|
+
checked += 1
|
|
235
|
+
issues = validate_skill(skill_dir)
|
|
236
|
+
errors = [issue for issue in issues if issue.level == "error"]
|
|
237
|
+
warnings = [issue for issue in issues if issue.level == "warning"]
|
|
238
|
+
for issue in errors:
|
|
239
|
+
self.output.error(f"{skill_dir.name}: {issue.message}")
|
|
240
|
+
for issue in warnings:
|
|
241
|
+
self.output.warning(f"{skill_dir.name}: {issue.message}")
|
|
242
|
+
if errors:
|
|
243
|
+
invalid += 1
|
|
244
|
+
status = "invalid" if errors else "valid with warnings" if warnings else "valid"
|
|
245
|
+
rows.append([str(skill_dir), status, str(len(errors)), str(len(warnings))])
|
|
246
|
+
|
|
247
|
+
if rows:
|
|
248
|
+
self.output.print_table(
|
|
249
|
+
["Skill", "Status", "Errors", "Warnings"], rows, title="Agent Skills validation"
|
|
250
|
+
)
|
|
251
|
+
if invalid:
|
|
252
|
+
self.output.error(f"{invalid} skill(s) failed validation.")
|
|
253
|
+
return 1
|
|
254
|
+
self.output.success(f"{checked} skill(s) conform to the Agent Skills specification.")
|
|
255
|
+
return 0
|
|
256
|
+
|
|
257
|
+
# ------------------------------------------------------------------
|
|
258
|
+
# Selection
|
|
259
|
+
# ------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
def _choose_agents(self, project_root: Path) -> tuple[_AgentTarget, ...] | None:
|
|
262
|
+
"""Offer a space-to-toggle list of agents. Returns None if cancelled."""
|
|
263
|
+
prompts = self.output.prompts()
|
|
264
|
+
prompts.require_interactive(
|
|
265
|
+
field="agent",
|
|
266
|
+
alternatives="Pass --agent codex|claude|github, or --all.",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
choices = [
|
|
270
|
+
Choice(
|
|
271
|
+
value=target.name,
|
|
272
|
+
title=target.name,
|
|
273
|
+
description=self._choice_description(project_root, target),
|
|
274
|
+
# Pre-check what is already installed, so confirming without
|
|
275
|
+
# touching anything reinstalls exactly what is already there.
|
|
276
|
+
checked=bool(self._existing_paths(project_root, target)),
|
|
277
|
+
)
|
|
278
|
+
for target in _TARGETS
|
|
279
|
+
]
|
|
280
|
+
names = prompts.checkbox(
|
|
281
|
+
"Which agents should get the 10xGraph skill?",
|
|
282
|
+
choices,
|
|
283
|
+
validate=lambda selected: bool(selected) or "Select at least one agent.",
|
|
284
|
+
)
|
|
285
|
+
if names is None:
|
|
286
|
+
return None
|
|
287
|
+
return tuple(_AGENT_LOOKUP[name.lower()] for name in names)
|
|
288
|
+
|
|
289
|
+
def _choice_description(self, project_root: Path, target: _AgentTarget) -> str:
|
|
290
|
+
install_root = target.artifacts[0].install_relpath
|
|
291
|
+
if self._existing_paths(project_root, target):
|
|
292
|
+
return f"{install_root} (installed)"
|
|
293
|
+
return install_root
|
|
294
|
+
|
|
295
|
+
@staticmethod
|
|
296
|
+
def _existing_paths(project_root: Path, target: _AgentTarget) -> list[Path]:
|
|
297
|
+
return [
|
|
298
|
+
project_root / artifact.install_relpath
|
|
299
|
+
for artifact in target.artifacts
|
|
300
|
+
if (project_root / artifact.install_relpath).exists()
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
def _confirm_overwrite(
|
|
304
|
+
self,
|
|
305
|
+
project_root: Path,
|
|
306
|
+
targets: tuple[_AgentTarget, ...],
|
|
307
|
+
*,
|
|
308
|
+
force: bool,
|
|
309
|
+
) -> bool:
|
|
310
|
+
"""Offer to overwrite in place of failing, when a human is present."""
|
|
311
|
+
if force:
|
|
312
|
+
return False
|
|
313
|
+
occupied = [t.name for t in targets if self._existing_paths(project_root, t)]
|
|
314
|
+
if not occupied:
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
prompts = self.output.prompts()
|
|
318
|
+
if not prompts.interactive:
|
|
319
|
+
# Non-interactive callers keep the previous contract: refuse and
|
|
320
|
+
# point at --force rather than silently replacing files.
|
|
321
|
+
return False
|
|
322
|
+
return bool(
|
|
323
|
+
prompts.confirm(
|
|
324
|
+
f"Overwrite the existing install for {', '.join(occupied)}?",
|
|
325
|
+
default=True,
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# ------------------------------------------------------------------
|
|
330
|
+
# Installation
|
|
331
|
+
# ------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
def _install_targets(
|
|
334
|
+
self,
|
|
335
|
+
templates_root: Path,
|
|
336
|
+
project_root: Path,
|
|
337
|
+
targets: tuple[_AgentTarget, ...],
|
|
338
|
+
*,
|
|
339
|
+
force: bool,
|
|
340
|
+
strict: bool,
|
|
341
|
+
) -> int:
|
|
342
|
+
installed: list[str] = []
|
|
343
|
+
skipped: list[str] = []
|
|
344
|
+
failed: list[str] = []
|
|
345
|
+
|
|
346
|
+
timeline = self.output.timeline(
|
|
347
|
+
"Installing 10xGraph skills",
|
|
348
|
+
steps=tuple((target.name.lower(), f"{target.name} skill") for target in targets),
|
|
349
|
+
)
|
|
350
|
+
with timeline:
|
|
351
|
+
for target in targets:
|
|
352
|
+
with timeline.step(target.name.lower()) as step:
|
|
353
|
+
existing = self._existing_paths(project_root, target)
|
|
354
|
+
if existing and not force:
|
|
355
|
+
if strict:
|
|
356
|
+
paths = ", ".join(str(dest) for dest in existing)
|
|
357
|
+
raise FileOperationError(
|
|
358
|
+
f"Skill already installed at {paths}. Use --force to overwrite.",
|
|
359
|
+
file_path=str(existing[0]),
|
|
360
|
+
)
|
|
361
|
+
step.skip("already installed — pass --force to overwrite")
|
|
362
|
+
skipped.append(target.name)
|
|
363
|
+
continue
|
|
364
|
+
try:
|
|
365
|
+
written = self._install_one(
|
|
366
|
+
templates_root, project_root, target, force=force
|
|
367
|
+
)
|
|
368
|
+
except (FileOperationError, OSError, UnicodeError) as exc:
|
|
369
|
+
# Record and continue: one unwritable target should not
|
|
370
|
+
# cancel the agents the user also asked for.
|
|
371
|
+
self.logger.error("Install failed for %s: %s", target.name, exc)
|
|
372
|
+
failed.append(f"{target.name}: {exc}")
|
|
373
|
+
step.fail(str(exc))
|
|
374
|
+
continue
|
|
375
|
+
installed.append(target.name)
|
|
376
|
+
step.detail(written[0])
|
|
377
|
+
|
|
378
|
+
for target in targets:
|
|
379
|
+
if target.name in installed:
|
|
380
|
+
self._print_activation_hint(target)
|
|
381
|
+
|
|
382
|
+
if skipped:
|
|
383
|
+
self.output.warning(
|
|
384
|
+
"Skipped existing installs (use --force to overwrite): " + ", ".join(skipped)
|
|
385
|
+
)
|
|
386
|
+
if failed:
|
|
387
|
+
self.output.error("Failed installs: " + "; ".join(failed))
|
|
388
|
+
if not installed:
|
|
389
|
+
return 1
|
|
390
|
+
|
|
391
|
+
self.output.completion_screen(
|
|
392
|
+
"Skills installed" if installed else "Nothing to install",
|
|
393
|
+
f"{len(installed)} agent(s) ready" if installed else "Every selection was skipped",
|
|
394
|
+
details={
|
|
395
|
+
"Installed": ", ".join(installed) or "none",
|
|
396
|
+
"Skipped": ", ".join(skipped) or "none",
|
|
397
|
+
"Project": project_root,
|
|
398
|
+
},
|
|
399
|
+
next_steps=["Restart your coding agent so it loads the new skill directory."]
|
|
400
|
+
if installed
|
|
401
|
+
else [],
|
|
402
|
+
)
|
|
403
|
+
return 0
|
|
404
|
+
|
|
405
|
+
def _install_one(
|
|
406
|
+
self,
|
|
407
|
+
templates_root: Path,
|
|
408
|
+
project_root: Path,
|
|
409
|
+
target: _AgentTarget,
|
|
410
|
+
*,
|
|
411
|
+
force: bool,
|
|
412
|
+
) -> list[str]:
|
|
413
|
+
installs = [
|
|
414
|
+
(
|
|
415
|
+
artifact,
|
|
416
|
+
templates_root / artifact.source_relpath,
|
|
417
|
+
project_root / artifact.install_relpath,
|
|
418
|
+
)
|
|
419
|
+
for artifact in target.artifacts
|
|
420
|
+
]
|
|
421
|
+
for _artifact, source, _dest in installs:
|
|
422
|
+
if not source.exists():
|
|
423
|
+
raise FileOperationError(
|
|
424
|
+
f"Bundled skills template not found: {source}", file_path=str(source)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
existing = [dest for _artifact, _source, dest in installs if dest.exists()]
|
|
428
|
+
if existing and not force:
|
|
429
|
+
paths = ", ".join(str(dest) for dest in existing)
|
|
430
|
+
raise FileOperationError(
|
|
431
|
+
f"Skill already installed at {paths}. Use --force to overwrite.",
|
|
432
|
+
file_path=str(existing[0]),
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
for _artifact, _source, dest in installs:
|
|
436
|
+
if dest.exists():
|
|
437
|
+
if dest.is_dir():
|
|
438
|
+
shutil.rmtree(dest)
|
|
439
|
+
else:
|
|
440
|
+
dest.unlink()
|
|
441
|
+
|
|
442
|
+
installed_paths: list[str] = []
|
|
443
|
+
for artifact, source, dest in installs:
|
|
444
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
445
|
+
|
|
446
|
+
if artifact.kind == "folder":
|
|
447
|
+
shutil.copytree(
|
|
448
|
+
source,
|
|
449
|
+
dest,
|
|
450
|
+
ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".DS_Store"),
|
|
451
|
+
)
|
|
452
|
+
if artifact.manifest:
|
|
453
|
+
self._write_manifest(dest, target.name)
|
|
454
|
+
else:
|
|
455
|
+
shutil.copyfile(source, dest)
|
|
456
|
+
installed_paths.append(str(dest))
|
|
457
|
+
|
|
458
|
+
return installed_paths
|
|
459
|
+
|
|
460
|
+
def _print_activation_hint(self, target: _AgentTarget) -> None:
|
|
461
|
+
"""Tell the user how to make the freshly installed skill take effect.
|
|
462
|
+
|
|
463
|
+
Coding agents load skills at session start. When ``10xgraph skills``
|
|
464
|
+
creates the skills directory for the first time during a running session,
|
|
465
|
+
the agent does not watch the new top-level directory until it is
|
|
466
|
+
restarted, so the skill appears "installed but unused". This note makes
|
|
467
|
+
the required restart explicit.
|
|
468
|
+
"""
|
|
469
|
+
if target.name == "Claude":
|
|
470
|
+
note = (
|
|
471
|
+
"Restart Claude Code (or run /exit then `claude`) so it loads the new "
|
|
472
|
+
".claude/skills/ directory. Claude auto-invokes the skill from its "
|
|
473
|
+
"description; type /10xgraph to run it manually."
|
|
474
|
+
)
|
|
475
|
+
elif target.name == "Codex":
|
|
476
|
+
note = (
|
|
477
|
+
"Restart Codex so it picks up the new .agents/skills/ directory. Codex "
|
|
478
|
+
"auto-selects the skill when your task matches its description."
|
|
479
|
+
)
|
|
480
|
+
else: # GitHub Copilot
|
|
481
|
+
note = (
|
|
482
|
+
"Restart GitHub Copilot / your editor so it loads the new "
|
|
483
|
+
".github/skills/ directory and .github/instructions/ file."
|
|
484
|
+
)
|
|
485
|
+
self.output.warning(f"Activate: {note}")
|
|
486
|
+
|
|
487
|
+
def _write_manifest(self, target_dir: Path, agent_name: str) -> None:
|
|
488
|
+
manifest = {
|
|
489
|
+
"agent": agent_name,
|
|
490
|
+
"cli_version": CLI_VERSION,
|
|
491
|
+
"installed_at": datetime.now(UTC).isoformat(timespec="seconds"),
|
|
492
|
+
}
|
|
493
|
+
(target_dir / _MANIFEST_FILENAME).write_text(
|
|
494
|
+
json.dumps(manifest, indent=2) + "\n", encoding="utf-8"
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
def _print_agents(self) -> None:
|
|
498
|
+
rows = [
|
|
499
|
+
[t.name, t.kind, ", ".join(a.install_relpath for a in t.artifacts)] for t in _TARGETS
|
|
500
|
+
]
|
|
501
|
+
self.output.print_table(
|
|
502
|
+
["Agent", "Kind", "Install path (relative to --path)"],
|
|
503
|
+
rows,
|
|
504
|
+
title="Supported agents",
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
def _safe_project_root(self, path: str) -> Path:
|
|
508
|
+
project_root = Path(path).resolve()
|
|
509
|
+
if project_root.parent == project_root:
|
|
510
|
+
raise ValidationError(
|
|
511
|
+
f"Refusing to install skills at filesystem root: {project_root}",
|
|
512
|
+
field="path",
|
|
513
|
+
)
|
|
514
|
+
if project_root == Path.home().resolve():
|
|
515
|
+
raise ValidationError(
|
|
516
|
+
f"Refusing to install skills directly into the home directory: {project_root}. "
|
|
517
|
+
"Pass --path pointing at a project directory.",
|
|
518
|
+
field="path",
|
|
519
|
+
)
|
|
520
|
+
return project_root
|
|
521
|
+
|
|
522
|
+
def _normalize_agent(self, value: str) -> _AgentTarget:
|
|
523
|
+
key = value.strip().lower()
|
|
524
|
+
if key in _AGENT_LOOKUP:
|
|
525
|
+
return _AGENT_LOOKUP[key]
|
|
526
|
+
|
|
527
|
+
valid = "Codex, Claude, GitHub, or 1, 2, 3"
|
|
528
|
+
raise ValidationError(f"Invalid agent '{value}'. Choose {valid}.", field="agent")
|
|
529
|
+
|
|
530
|
+
def _templates_root(self) -> Path:
|
|
531
|
+
cli_dir = Path(__file__).resolve().parents[1]
|
|
532
|
+
return cli_dir / "templates" / "skills"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Test command implementation — thin pytest wrapper."""
|
|
2
|
+
|
|
3
|
+
import subprocess # nosec: B404
|
|
4
|
+
import sys
|
|
5
|
+
import webbrowser
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from tenxgraph_api.cli.commands import BaseCommand
|
|
10
|
+
from tenxgraph_api.cli.core.config import ConfigManager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestCommand(BaseCommand):
|
|
14
|
+
"""Run the project's test suite via pytest."""
|
|
15
|
+
|
|
16
|
+
def execute(
|
|
17
|
+
self,
|
|
18
|
+
path: str | None = None,
|
|
19
|
+
coverage: bool = False,
|
|
20
|
+
html: bool = False,
|
|
21
|
+
keyword: str | None = None,
|
|
22
|
+
verbose: bool = False,
|
|
23
|
+
quiet: bool = False,
|
|
24
|
+
extra_args: tuple[str, ...] = (),
|
|
25
|
+
**kwargs: Any,
|
|
26
|
+
) -> int:
|
|
27
|
+
project_root = Path.cwd()
|
|
28
|
+
self.output.command_header("test", f"Running the project test suite in {project_root}")
|
|
29
|
+
|
|
30
|
+
timeline = self.output.timeline(
|
|
31
|
+
"Preparing the test run",
|
|
32
|
+
steps=(
|
|
33
|
+
("config", "Resolving test configuration"),
|
|
34
|
+
("command", "Building the pytest command"),
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
with timeline:
|
|
38
|
+
with timeline.step("config") as step:
|
|
39
|
+
# Load optional overrides from 10xgraph.json
|
|
40
|
+
cfg: dict[str, Any] = {}
|
|
41
|
+
config_manager = ConfigManager()
|
|
42
|
+
discovered = config_manager.auto_discover_config()
|
|
43
|
+
if discovered:
|
|
44
|
+
try:
|
|
45
|
+
config_manager.load_config(str(discovered))
|
|
46
|
+
cfg = config_manager.get_test_config()
|
|
47
|
+
step.detail(str(discovered))
|
|
48
|
+
except Exception: # nosec: B110
|
|
49
|
+
self.logger.warning("Failed to load test configuration from %s", discovered)
|
|
50
|
+
step.detail(f"ignored unreadable config at {discovered}")
|
|
51
|
+
else:
|
|
52
|
+
step.detail("no 10xgraph.json found — using pytest defaults")
|
|
53
|
+
|
|
54
|
+
# Explicit CLI path wins; 10xgraph.json next; None = pytest auto-discovery
|
|
55
|
+
resolved_path: str | None = path or cfg.get("path") or None
|
|
56
|
+
resolved_coverage = coverage or cfg.get("coverage", False)
|
|
57
|
+
coverage_threshold: int | None = cfg.get("coverage_threshold")
|
|
58
|
+
location = str(project_root / resolved_path) if resolved_path else str(project_root)
|
|
59
|
+
|
|
60
|
+
with timeline.step("command") as step:
|
|
61
|
+
cmd = [sys.executable, "-m", "pytest"]
|
|
62
|
+
if resolved_path:
|
|
63
|
+
cmd.append(resolved_path)
|
|
64
|
+
|
|
65
|
+
if quiet and not verbose:
|
|
66
|
+
cmd.append("-q")
|
|
67
|
+
else:
|
|
68
|
+
cmd.append("-v")
|
|
69
|
+
|
|
70
|
+
if resolved_coverage:
|
|
71
|
+
cmd += [
|
|
72
|
+
"--cov=.",
|
|
73
|
+
"--cov-report=term-missing",
|
|
74
|
+
"--cov-report=html:htmlcov",
|
|
75
|
+
]
|
|
76
|
+
if coverage_threshold is not None:
|
|
77
|
+
cmd.append(f"--cov-fail-under={coverage_threshold}")
|
|
78
|
+
|
|
79
|
+
if keyword:
|
|
80
|
+
cmd += ["-k", keyword]
|
|
81
|
+
|
|
82
|
+
cmd += list(extra_args)
|
|
83
|
+
step.detail(" ".join(["pytest", *cmd[3:]]))
|
|
84
|
+
|
|
85
|
+
self.logger.info("Running: %s", " ".join(cmd))
|
|
86
|
+
|
|
87
|
+
# No spinner around this: pytest owns the terminal while it streams its
|
|
88
|
+
# own progress, and a live region would fight it for the cursor.
|
|
89
|
+
result = subprocess.run(cmd, cwd=project_root, check=False) # nosec: B603 # noqa: S603
|
|
90
|
+
|
|
91
|
+
if result.returncode == 0:
|
|
92
|
+
self.output.completion_screen(
|
|
93
|
+
"Tests passed",
|
|
94
|
+
"The project test suite completed successfully",
|
|
95
|
+
details={
|
|
96
|
+
"Location": location,
|
|
97
|
+
"Coverage": "enabled" if resolved_coverage else "disabled",
|
|
98
|
+
},
|
|
99
|
+
next_steps=(
|
|
100
|
+
["Open htmlcov/index.html to inspect the coverage report."]
|
|
101
|
+
if resolved_coverage
|
|
102
|
+
else []
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
self.output.error(f"Tests finished with exit code {result.returncode}.")
|
|
107
|
+
|
|
108
|
+
if html and resolved_coverage:
|
|
109
|
+
report_path = (project_root / "htmlcov" / "index.html").as_uri()
|
|
110
|
+
self.output.info(f"Opening coverage report: {report_path}", emoji=False)
|
|
111
|
+
webbrowser.open(report_path)
|
|
112
|
+
|
|
113
|
+
return result.returncode
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Version command implementation."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError
|
|
4
|
+
from importlib.metadata import version as _pkg_version
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from tenxgraph_api.cli.commands import BaseCommand
|
|
8
|
+
from tenxgraph_api.cli.constants import CLI_VERSION
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VersionCommand(BaseCommand):
|
|
12
|
+
"""Command to display version information."""
|
|
13
|
+
|
|
14
|
+
def execute(self, **kwargs: Any) -> int:
|
|
15
|
+
"""Execute the version command.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Exit code
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
self.output.command_header(
|
|
22
|
+
"version",
|
|
23
|
+
"Show 10xGraph CLI and package version info",
|
|
24
|
+
color="green",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
core_version = self._core_version()
|
|
28
|
+
|
|
29
|
+
self.output.success(f"10xgraph-api\n Version: {CLI_VERSION}")
|
|
30
|
+
self.output.info(f"10xgraph (core)\n Version: {core_version}")
|
|
31
|
+
|
|
32
|
+
return 0
|
|
33
|
+
|
|
34
|
+
except Exception as e:
|
|
35
|
+
return self.handle_error(e)
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _core_version() -> str:
|
|
39
|
+
"""Resolve the installed core framework version.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Version string, or ``"not installed"`` when the core package is absent.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
return _pkg_version("10xgraph")
|
|
46
|
+
except PackageNotFoundError:
|
|
47
|
+
return "not installed"
|