htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +51 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/__init__.py +8 -1
- htmlgraph/analytics/cli.py +86 -20
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +67 -27
- htmlgraph/analytics_index.py +53 -20
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/base.py +57 -2
- htmlgraph/builders/bug.py +19 -3
- htmlgraph/builders/chore.py +19 -3
- htmlgraph/builders/epic.py +19 -3
- htmlgraph/builders/feature.py +27 -3
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +19 -3
- htmlgraph/builders/spike.py +29 -3
- htmlgraph/builders/track.py +42 -1
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +197 -14
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +13 -2
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +116 -7
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2246 -248
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/deploy.py +26 -27
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +2 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +86 -37
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +67 -9
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +790 -99
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +327 -76
- htmlgraph/hooks/orchestrator_reflector.py +31 -4
- htmlgraph/hooks/post_tool_use_failure.py +32 -7
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +92 -19
- htmlgraph/hooks/pretooluse.py +527 -7
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +99 -4
- htmlgraph/hooks/validator.py +212 -91
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +125 -100
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +217 -18
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +115 -4
- htmlgraph/parallel.py +2 -1
- htmlgraph/parser.py +86 -6
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +2 -1
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +295 -107
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +285 -3
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +33 -1
- htmlgraph/track_manager.py +38 -0
- htmlgraph/transcript.py +18 -5
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -4839
- htmlgraph/sdk.py +0 -2359
- htmlgraph-0.20.1.dist-info/RECORD +0 -118
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""HtmlGraph CLI - Browse command for opening dashboard in browser."""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import webbrowser
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from htmlgraph.cli.base import BaseCommand, CommandResult
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BrowseCommand(BaseCommand):
|
|
17
|
+
"""Open the HtmlGraph dashboard in your default browser.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
htmlgraph browse # Open dashboard
|
|
21
|
+
htmlgraph browse --port 8080 # Custom port
|
|
22
|
+
htmlgraph browse --query-type feature # Show only features
|
|
23
|
+
htmlgraph browse --query-status todo # Show only todo items
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
port: int = 8080,
|
|
30
|
+
query_type: str | None = None,
|
|
31
|
+
query_status: str | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Initialize BrowseCommand.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
port: Server port (default: 8080)
|
|
37
|
+
query_type: Filter by type (feature, track, bug, spike, chore, epic)
|
|
38
|
+
query_status: Filter by status (todo, in_progress, blocked, done)
|
|
39
|
+
"""
|
|
40
|
+
super().__init__()
|
|
41
|
+
self.port = port
|
|
42
|
+
self.query_type = query_type
|
|
43
|
+
self.query_status = query_status
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_args(cls, args: argparse.Namespace) -> BrowseCommand:
|
|
47
|
+
"""Create BrowseCommand from argparse arguments.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
args: Argparse namespace with command arguments
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
BrowseCommand instance
|
|
54
|
+
"""
|
|
55
|
+
return cls(
|
|
56
|
+
port=args.port,
|
|
57
|
+
query_type=args.query_type,
|
|
58
|
+
query_status=args.query_status,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def execute(self) -> CommandResult:
|
|
62
|
+
"""Execute the browse command.
|
|
63
|
+
|
|
64
|
+
Opens the dashboard in the default browser with optional query parameters.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
CommandResult with success status and URL
|
|
68
|
+
"""
|
|
69
|
+
# Build URL with query params
|
|
70
|
+
url = f"http://localhost:{self.port}"
|
|
71
|
+
|
|
72
|
+
params = []
|
|
73
|
+
if self.query_type:
|
|
74
|
+
params.append(f"type={self.query_type}")
|
|
75
|
+
if self.query_status:
|
|
76
|
+
params.append(f"status={self.query_status}")
|
|
77
|
+
|
|
78
|
+
if params:
|
|
79
|
+
url += "?" + "&".join(params)
|
|
80
|
+
|
|
81
|
+
# Check if server is running
|
|
82
|
+
try:
|
|
83
|
+
import requests # type: ignore[import-untyped]
|
|
84
|
+
|
|
85
|
+
response = requests.head(f"http://localhost:{self.port}", timeout=1)
|
|
86
|
+
response.raise_for_status()
|
|
87
|
+
except ImportError:
|
|
88
|
+
# requests module not available - try to open anyway with a warning
|
|
89
|
+
webbrowser.open(url)
|
|
90
|
+
return CommandResult(
|
|
91
|
+
data={"url": url},
|
|
92
|
+
text=f"Opening dashboard at {url}\n(Note: Could not verify server is running - install 'requests' for server checks)",
|
|
93
|
+
exit_code=0,
|
|
94
|
+
)
|
|
95
|
+
except Exception:
|
|
96
|
+
# Server not running or not responding
|
|
97
|
+
return CommandResult(
|
|
98
|
+
text=f"Dashboard server not running on port {self.port}.\nStart with: htmlgraph serve --port {self.port}",
|
|
99
|
+
exit_code=1,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Open browser
|
|
103
|
+
try:
|
|
104
|
+
webbrowser.open(url)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
return CommandResult(
|
|
107
|
+
text=f"Failed to open browser: {e}\nYou can manually visit: {url}",
|
|
108
|
+
exit_code=1,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return CommandResult(
|
|
112
|
+
data={"url": url},
|
|
113
|
+
text=f"Opening dashboard at {url}",
|
|
114
|
+
exit_code=0,
|
|
115
|
+
)
|
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""HtmlGraph CLI - Feature management commands."""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from rich import box
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from htmlgraph.cli.base import BaseCommand, CommandError, CommandResult
|
|
15
|
+
from htmlgraph.cli.constants import DEFAULT_GRAPH_DIR
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from argparse import _SubParsersAction
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def register_feature_commands(subparsers: _SubParsersAction) -> None:
|
|
24
|
+
"""Register feature management commands."""
|
|
25
|
+
feature_parser = subparsers.add_parser("feature", help="Feature management")
|
|
26
|
+
feature_subparsers = feature_parser.add_subparsers(
|
|
27
|
+
dest="feature_command", help="Feature command"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# feature list
|
|
31
|
+
feature_list = feature_subparsers.add_parser("list", help="List all features")
|
|
32
|
+
feature_list.add_argument(
|
|
33
|
+
"--status",
|
|
34
|
+
choices=["todo", "in_progress", "completed", "blocked"],
|
|
35
|
+
help="Filter by status",
|
|
36
|
+
)
|
|
37
|
+
feature_list.add_argument(
|
|
38
|
+
"--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
|
|
39
|
+
)
|
|
40
|
+
feature_list.add_argument(
|
|
41
|
+
"--format", choices=["json", "text"], default="text", help="Output format"
|
|
42
|
+
)
|
|
43
|
+
feature_list.add_argument(
|
|
44
|
+
"--quiet", "-q", action="store_true", help="Suppress empty output"
|
|
45
|
+
)
|
|
46
|
+
feature_list.set_defaults(func=FeatureListCommand.from_args)
|
|
47
|
+
|
|
48
|
+
# feature create
|
|
49
|
+
feature_create = feature_subparsers.add_parser(
|
|
50
|
+
"create", help="Create a new feature"
|
|
51
|
+
)
|
|
52
|
+
feature_create.add_argument("title", help="Feature title")
|
|
53
|
+
feature_create.add_argument("--description", help="Feature description")
|
|
54
|
+
feature_create.add_argument(
|
|
55
|
+
"--priority", choices=["low", "medium", "high", "critical"], default="medium"
|
|
56
|
+
)
|
|
57
|
+
feature_create.add_argument("--steps", type=int, help="Number of steps")
|
|
58
|
+
feature_create.add_argument(
|
|
59
|
+
"--collection", default="features", help="Collection name"
|
|
60
|
+
)
|
|
61
|
+
feature_create.add_argument("--track", help="Track ID to link feature to")
|
|
62
|
+
feature_create.add_argument("--agent", default="claude-code", help="Agent name")
|
|
63
|
+
feature_create.add_argument(
|
|
64
|
+
"--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
|
|
65
|
+
)
|
|
66
|
+
feature_create.add_argument(
|
|
67
|
+
"--format", choices=["json", "text"], default="text", help="Output format"
|
|
68
|
+
)
|
|
69
|
+
feature_create.set_defaults(func=FeatureCreateCommand.from_args)
|
|
70
|
+
|
|
71
|
+
# feature start
|
|
72
|
+
feature_start = feature_subparsers.add_parser(
|
|
73
|
+
"start", help="Start working on a feature"
|
|
74
|
+
)
|
|
75
|
+
feature_start.add_argument("id", help="Feature ID")
|
|
76
|
+
feature_start.add_argument(
|
|
77
|
+
"--collection", default="features", help="Collection name"
|
|
78
|
+
)
|
|
79
|
+
feature_start.add_argument("--agent", default="claude-code", help="Agent name")
|
|
80
|
+
feature_start.add_argument(
|
|
81
|
+
"--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
|
|
82
|
+
)
|
|
83
|
+
feature_start.add_argument(
|
|
84
|
+
"--format", choices=["json", "text"], default="text", help="Output format"
|
|
85
|
+
)
|
|
86
|
+
feature_start.set_defaults(func=FeatureStartCommand.from_args)
|
|
87
|
+
|
|
88
|
+
# feature complete
|
|
89
|
+
feature_complete = feature_subparsers.add_parser(
|
|
90
|
+
"complete", help="Mark feature as completed"
|
|
91
|
+
)
|
|
92
|
+
feature_complete.add_argument("id", help="Feature ID")
|
|
93
|
+
feature_complete.add_argument(
|
|
94
|
+
"--collection", default="features", help="Collection name"
|
|
95
|
+
)
|
|
96
|
+
feature_complete.add_argument("--agent", default="claude-code", help="Agent name")
|
|
97
|
+
feature_complete.add_argument(
|
|
98
|
+
"--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
|
|
99
|
+
)
|
|
100
|
+
feature_complete.add_argument(
|
|
101
|
+
"--format", choices=["json", "text"], default="text", help="Output format"
|
|
102
|
+
)
|
|
103
|
+
feature_complete.set_defaults(func=FeatureCompleteCommand.from_args)
|
|
104
|
+
|
|
105
|
+
# feature claim
|
|
106
|
+
feature_claim = feature_subparsers.add_parser("claim", help="Claim a feature")
|
|
107
|
+
feature_claim.add_argument("id", help="Feature ID")
|
|
108
|
+
feature_claim.add_argument(
|
|
109
|
+
"--collection", default="features", help="Collection name"
|
|
110
|
+
)
|
|
111
|
+
feature_claim.add_argument("--agent", default="claude-code", help="Agent name")
|
|
112
|
+
feature_claim.add_argument(
|
|
113
|
+
"--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
|
|
114
|
+
)
|
|
115
|
+
feature_claim.add_argument(
|
|
116
|
+
"--format", choices=["json", "text"], default="text", help="Output format"
|
|
117
|
+
)
|
|
118
|
+
feature_claim.set_defaults(func=FeatureClaimCommand.from_args)
|
|
119
|
+
|
|
120
|
+
# feature release
|
|
121
|
+
feature_release = feature_subparsers.add_parser("release", help="Release a feature")
|
|
122
|
+
feature_release.add_argument("id", help="Feature ID")
|
|
123
|
+
feature_release.add_argument(
|
|
124
|
+
"--collection", default="features", help="Collection name"
|
|
125
|
+
)
|
|
126
|
+
feature_release.add_argument("--agent", default="claude-code", help="Agent name")
|
|
127
|
+
feature_release.add_argument(
|
|
128
|
+
"--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
|
|
129
|
+
)
|
|
130
|
+
feature_release.add_argument(
|
|
131
|
+
"--format", choices=["json", "text"], default="text", help="Output format"
|
|
132
|
+
)
|
|
133
|
+
feature_release.set_defaults(func=FeatureReleaseCommand.from_args)
|
|
134
|
+
|
|
135
|
+
# feature primary
|
|
136
|
+
feature_primary = feature_subparsers.add_parser(
|
|
137
|
+
"primary", help="Set primary feature"
|
|
138
|
+
)
|
|
139
|
+
feature_primary.add_argument("id", help="Feature ID")
|
|
140
|
+
feature_primary.add_argument(
|
|
141
|
+
"--collection", default="features", help="Collection name"
|
|
142
|
+
)
|
|
143
|
+
feature_primary.add_argument("--agent", default="claude-code", help="Agent name")
|
|
144
|
+
feature_primary.add_argument(
|
|
145
|
+
"--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
|
|
146
|
+
)
|
|
147
|
+
feature_primary.add_argument(
|
|
148
|
+
"--format", choices=["json", "text"], default="text", help="Output format"
|
|
149
|
+
)
|
|
150
|
+
feature_primary.set_defaults(func=FeaturePrimaryCommand.from_args)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ============================================================================
|
|
154
|
+
# Feature Commands
|
|
155
|
+
# ============================================================================
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class FeatureListCommand(BaseCommand):
|
|
159
|
+
"""List all features."""
|
|
160
|
+
|
|
161
|
+
def __init__(self, *, status: str | None, quiet: bool) -> None:
|
|
162
|
+
super().__init__()
|
|
163
|
+
self.status = status
|
|
164
|
+
self.quiet = quiet
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def from_args(cls, args: argparse.Namespace) -> FeatureListCommand:
|
|
168
|
+
# Validate inputs using FeatureFilter model
|
|
169
|
+
from htmlgraph.cli.models import FeatureFilter
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
filter_model = FeatureFilter(status=args.status)
|
|
173
|
+
except ValueError as e:
|
|
174
|
+
raise CommandError(str(e))
|
|
175
|
+
|
|
176
|
+
return cls(
|
|
177
|
+
status=filter_model.status,
|
|
178
|
+
quiet=args.quiet,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def execute(self) -> CommandResult:
|
|
182
|
+
"""List all features."""
|
|
183
|
+
from htmlgraph.cli.models import FeatureDisplay
|
|
184
|
+
from htmlgraph.converter import node_to_dict
|
|
185
|
+
|
|
186
|
+
sdk = self.get_sdk()
|
|
187
|
+
|
|
188
|
+
# Query features with SDK
|
|
189
|
+
if self.status:
|
|
190
|
+
nodes = sdk.features.where(status=self.status)
|
|
191
|
+
else:
|
|
192
|
+
nodes = sdk.features.all()
|
|
193
|
+
|
|
194
|
+
# Convert to display models for type-safe sorting
|
|
195
|
+
display_features = [FeatureDisplay.from_node(n) for n in nodes]
|
|
196
|
+
|
|
197
|
+
# Sort by priority then updated using display model's sort_key
|
|
198
|
+
display_features.sort(key=lambda f: f.sort_key(), reverse=True)
|
|
199
|
+
|
|
200
|
+
if not display_features:
|
|
201
|
+
if not self.quiet:
|
|
202
|
+
from htmlgraph.cli.base import TextOutputBuilder
|
|
203
|
+
|
|
204
|
+
status_msg = f"with status '{self.status}'" if self.status else ""
|
|
205
|
+
output = TextOutputBuilder()
|
|
206
|
+
output.add_warning(f"No features found {status_msg}.")
|
|
207
|
+
return CommandResult(text=output.build(), json_data={"features": []})
|
|
208
|
+
return CommandResult(json_data={"features": []})
|
|
209
|
+
|
|
210
|
+
# Create Rich table
|
|
211
|
+
table = Table(
|
|
212
|
+
title="Features",
|
|
213
|
+
show_header=True,
|
|
214
|
+
header_style="bold magenta",
|
|
215
|
+
box=box.ROUNDED,
|
|
216
|
+
)
|
|
217
|
+
table.add_column("ID", style="cyan", no_wrap=False, max_width=20)
|
|
218
|
+
table.add_column("Title", style="yellow", max_width=40)
|
|
219
|
+
table.add_column("Status", style="green", width=12)
|
|
220
|
+
table.add_column("Priority", style="blue", width=10)
|
|
221
|
+
table.add_column("Updated", style="white", width=16)
|
|
222
|
+
|
|
223
|
+
for feature in display_features:
|
|
224
|
+
table.add_row(
|
|
225
|
+
feature.id,
|
|
226
|
+
feature.title,
|
|
227
|
+
feature.status,
|
|
228
|
+
feature.priority,
|
|
229
|
+
feature.updated_str,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Return table object directly - TextFormatter will print it properly
|
|
233
|
+
return CommandResult(
|
|
234
|
+
data=table,
|
|
235
|
+
json_data=[node_to_dict(n) for n in nodes],
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class FeatureCreateCommand(BaseCommand):
|
|
240
|
+
"""Create a new feature."""
|
|
241
|
+
|
|
242
|
+
def __init__(
|
|
243
|
+
self,
|
|
244
|
+
*,
|
|
245
|
+
title: str,
|
|
246
|
+
description: str | None,
|
|
247
|
+
priority: str,
|
|
248
|
+
steps: int | None,
|
|
249
|
+
collection: str,
|
|
250
|
+
track_id: str | None,
|
|
251
|
+
) -> None:
|
|
252
|
+
super().__init__()
|
|
253
|
+
self.title = title
|
|
254
|
+
self.description = description
|
|
255
|
+
self.priority = priority
|
|
256
|
+
self.steps = steps
|
|
257
|
+
self.collection = collection
|
|
258
|
+
self.track_id = track_id
|
|
259
|
+
|
|
260
|
+
@classmethod
|
|
261
|
+
def from_args(cls, args: argparse.Namespace) -> FeatureCreateCommand:
|
|
262
|
+
return cls(
|
|
263
|
+
title=args.title,
|
|
264
|
+
description=args.description,
|
|
265
|
+
priority=args.priority,
|
|
266
|
+
steps=args.steps,
|
|
267
|
+
collection=args.collection,
|
|
268
|
+
track_id=args.track,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def execute(self) -> CommandResult:
|
|
272
|
+
"""Create a new feature."""
|
|
273
|
+
from rich.prompt import Prompt
|
|
274
|
+
|
|
275
|
+
from htmlgraph.converter import node_to_dict
|
|
276
|
+
|
|
277
|
+
sdk = self.get_sdk()
|
|
278
|
+
|
|
279
|
+
# Convert steps count to list of step names
|
|
280
|
+
step_names = None
|
|
281
|
+
if self.steps:
|
|
282
|
+
step_names = [f"Step {i + 1}" for i in range(self.steps)]
|
|
283
|
+
|
|
284
|
+
# Determine track_id for feature creation
|
|
285
|
+
track_id = self.track_id
|
|
286
|
+
|
|
287
|
+
# Only enforce track selection for main features collection
|
|
288
|
+
if self.collection == "features":
|
|
289
|
+
if not track_id:
|
|
290
|
+
# Get available tracks
|
|
291
|
+
try:
|
|
292
|
+
tracks = sdk.tracks.all()
|
|
293
|
+
if not tracks:
|
|
294
|
+
raise CommandError(
|
|
295
|
+
"No tracks found. Create a track first:\n"
|
|
296
|
+
" uv run htmlgraph track new 'Track Title'"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if len(tracks) == 1:
|
|
300
|
+
# Auto-select if only one track exists
|
|
301
|
+
track_id = tracks[0].id
|
|
302
|
+
console.print(
|
|
303
|
+
f"[dim]Auto-selected track: {tracks[0].title}[/dim]"
|
|
304
|
+
)
|
|
305
|
+
else:
|
|
306
|
+
# Interactive selection
|
|
307
|
+
console.print("[bold]Available Tracks:[/bold]")
|
|
308
|
+
for i, track in enumerate(tracks, 1):
|
|
309
|
+
console.print(f" {i}. {track.title} ({track.id})")
|
|
310
|
+
|
|
311
|
+
selection = Prompt.ask(
|
|
312
|
+
"Select track",
|
|
313
|
+
choices=[str(i) for i in range(1, len(tracks) + 1)],
|
|
314
|
+
)
|
|
315
|
+
track_id = tracks[int(selection) - 1].id
|
|
316
|
+
except Exception as e:
|
|
317
|
+
raise CommandError(f"Failed to get available tracks: {e}")
|
|
318
|
+
|
|
319
|
+
builder = sdk.features.create(
|
|
320
|
+
title=self.title,
|
|
321
|
+
description=self.description or "",
|
|
322
|
+
priority=self.priority,
|
|
323
|
+
)
|
|
324
|
+
if step_names:
|
|
325
|
+
builder.add_steps(step_names)
|
|
326
|
+
if track_id:
|
|
327
|
+
builder.set_track(track_id)
|
|
328
|
+
node = builder.save()
|
|
329
|
+
else:
|
|
330
|
+
node = sdk.session_manager.create_feature(
|
|
331
|
+
title=self.title,
|
|
332
|
+
collection=self.collection,
|
|
333
|
+
description=self.description or "",
|
|
334
|
+
priority=self.priority,
|
|
335
|
+
steps=step_names,
|
|
336
|
+
agent=self.agent,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Create Rich table for output
|
|
340
|
+
table = Table(show_header=False, box=None)
|
|
341
|
+
table.add_column(style="bold cyan")
|
|
342
|
+
table.add_column()
|
|
343
|
+
|
|
344
|
+
table.add_row("Created:", f"[green]{node.id}[/green]")
|
|
345
|
+
table.add_row("Title:", f"[yellow]{node.title}[/yellow]")
|
|
346
|
+
table.add_row("Status:", f"[blue]{node.status}[/blue]")
|
|
347
|
+
if node.track_id:
|
|
348
|
+
table.add_row("Track:", f"[cyan]{node.track_id}[/cyan]")
|
|
349
|
+
table.add_row(
|
|
350
|
+
"Path:", f"[dim]{self.graph_dir}/{self.collection}/{node.id}.html[/dim]"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Return table object directly - TextFormatter will print it properly
|
|
354
|
+
return CommandResult(
|
|
355
|
+
data=table,
|
|
356
|
+
json_data=node_to_dict(node),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class FeatureStartCommand(BaseCommand):
|
|
361
|
+
"""Start working on a feature."""
|
|
362
|
+
|
|
363
|
+
def __init__(self, *, feature_id: str, collection: str) -> None:
|
|
364
|
+
super().__init__()
|
|
365
|
+
self.feature_id = feature_id
|
|
366
|
+
self.collection = collection
|
|
367
|
+
|
|
368
|
+
@classmethod
|
|
369
|
+
def from_args(cls, args: argparse.Namespace) -> FeatureStartCommand:
|
|
370
|
+
return cls(feature_id=args.id, collection=args.collection)
|
|
371
|
+
|
|
372
|
+
def execute(self) -> CommandResult:
|
|
373
|
+
"""Start working on a feature."""
|
|
374
|
+
from htmlgraph.converter import node_to_dict
|
|
375
|
+
|
|
376
|
+
sdk = self.get_sdk()
|
|
377
|
+
collection = getattr(sdk, self.collection, None)
|
|
378
|
+
self.require_collection(collection, self.collection)
|
|
379
|
+
assert collection is not None # Type narrowing for mypy
|
|
380
|
+
|
|
381
|
+
node = collection.start(self.feature_id)
|
|
382
|
+
self.require_node(node, "feature", self.feature_id)
|
|
383
|
+
|
|
384
|
+
status = sdk.session_manager.get_status()
|
|
385
|
+
|
|
386
|
+
# Create Rich table for output
|
|
387
|
+
table = Table(show_header=False, box=None)
|
|
388
|
+
table.add_column(style="bold cyan")
|
|
389
|
+
table.add_column()
|
|
390
|
+
|
|
391
|
+
table.add_row("Started:", f"[green]{node.id}[/green]")
|
|
392
|
+
table.add_row("Title:", f"[yellow]{node.title}[/yellow]")
|
|
393
|
+
table.add_row("Status:", f"[blue]{node.status}[/blue]")
|
|
394
|
+
wip_color = "red" if status["wip_count"] >= status["wip_limit"] else "green"
|
|
395
|
+
table.add_row(
|
|
396
|
+
"WIP:",
|
|
397
|
+
f"[{wip_color}]{status['wip_count']}/{status['wip_limit']}[/{wip_color}]",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Return table object directly - TextFormatter will print it properly
|
|
401
|
+
return CommandResult(
|
|
402
|
+
data=table,
|
|
403
|
+
json_data=node_to_dict(node),
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class FeatureCompleteCommand(BaseCommand):
|
|
408
|
+
"""Mark feature as completed."""
|
|
409
|
+
|
|
410
|
+
def __init__(self, *, feature_id: str, collection: str) -> None:
|
|
411
|
+
super().__init__()
|
|
412
|
+
self.feature_id = feature_id
|
|
413
|
+
self.collection = collection
|
|
414
|
+
|
|
415
|
+
@classmethod
|
|
416
|
+
def from_args(cls, args: argparse.Namespace) -> FeatureCompleteCommand:
|
|
417
|
+
return cls(feature_id=args.id, collection=args.collection)
|
|
418
|
+
|
|
419
|
+
def execute(self) -> CommandResult:
|
|
420
|
+
"""Mark feature as completed."""
|
|
421
|
+
from htmlgraph.converter import node_to_dict
|
|
422
|
+
|
|
423
|
+
sdk = self.get_sdk()
|
|
424
|
+
collection = getattr(sdk, self.collection, None)
|
|
425
|
+
self.require_collection(collection, self.collection)
|
|
426
|
+
assert collection is not None # Type narrowing for mypy
|
|
427
|
+
|
|
428
|
+
node = collection.complete(self.feature_id)
|
|
429
|
+
self.require_node(node, "feature", self.feature_id)
|
|
430
|
+
|
|
431
|
+
# Create Rich panel for output
|
|
432
|
+
panel = Panel(
|
|
433
|
+
f"[bold green]✓ Completed[/bold green]\n"
|
|
434
|
+
f"[cyan]{node.id}[/cyan]\n"
|
|
435
|
+
f"[yellow]{node.title}[/yellow]",
|
|
436
|
+
border_style="green",
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# Return panel object directly - TextFormatter will print it properly
|
|
440
|
+
return CommandResult(
|
|
441
|
+
data=panel,
|
|
442
|
+
json_data=node_to_dict(node),
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class FeatureClaimCommand(BaseCommand):
|
|
447
|
+
"""Claim a feature."""
|
|
448
|
+
|
|
449
|
+
def __init__(self, *, feature_id: str, collection: str) -> None:
|
|
450
|
+
super().__init__()
|
|
451
|
+
self.feature_id = feature_id
|
|
452
|
+
self.collection = collection
|
|
453
|
+
|
|
454
|
+
@classmethod
|
|
455
|
+
def from_args(cls, args: argparse.Namespace) -> FeatureClaimCommand:
|
|
456
|
+
return cls(feature_id=args.id, collection=args.collection)
|
|
457
|
+
|
|
458
|
+
def execute(self) -> CommandResult:
|
|
459
|
+
"""Claim a feature."""
|
|
460
|
+
from htmlgraph.converter import node_to_dict
|
|
461
|
+
|
|
462
|
+
sdk = self.get_sdk()
|
|
463
|
+
collection = getattr(sdk, self.collection, None)
|
|
464
|
+
self.require_collection(collection, self.collection)
|
|
465
|
+
assert collection is not None # Type narrowing for mypy
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
node = collection.claim(self.feature_id)
|
|
469
|
+
except ValueError as e:
|
|
470
|
+
raise CommandError(str(e))
|
|
471
|
+
|
|
472
|
+
self.require_node(node, "feature", self.feature_id)
|
|
473
|
+
|
|
474
|
+
from htmlgraph.cli.base import TextOutputBuilder
|
|
475
|
+
|
|
476
|
+
output = TextOutputBuilder()
|
|
477
|
+
output.add_success(f"Claimed: {node.id}")
|
|
478
|
+
output.add_field("Agent", node.agent_assigned)
|
|
479
|
+
output.add_field("Session", node.claimed_by_session)
|
|
480
|
+
|
|
481
|
+
return CommandResult(
|
|
482
|
+
data=node_to_dict(node),
|
|
483
|
+
text=output.build(),
|
|
484
|
+
json_data=node_to_dict(node),
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class FeatureReleaseCommand(BaseCommand):
|
|
489
|
+
"""Release a feature."""
|
|
490
|
+
|
|
491
|
+
def __init__(self, *, feature_id: str, collection: str) -> None:
|
|
492
|
+
super().__init__()
|
|
493
|
+
self.feature_id = feature_id
|
|
494
|
+
self.collection = collection
|
|
495
|
+
|
|
496
|
+
@classmethod
|
|
497
|
+
def from_args(cls, args: argparse.Namespace) -> FeatureReleaseCommand:
|
|
498
|
+
return cls(feature_id=args.id, collection=args.collection)
|
|
499
|
+
|
|
500
|
+
def execute(self) -> CommandResult:
|
|
501
|
+
"""Release a feature."""
|
|
502
|
+
from htmlgraph.converter import node_to_dict
|
|
503
|
+
|
|
504
|
+
sdk = self.get_sdk()
|
|
505
|
+
collection = getattr(sdk, self.collection, None)
|
|
506
|
+
self.require_collection(collection, self.collection)
|
|
507
|
+
assert collection is not None # Type narrowing for mypy
|
|
508
|
+
|
|
509
|
+
try:
|
|
510
|
+
node = collection.release(self.feature_id)
|
|
511
|
+
except ValueError as e:
|
|
512
|
+
raise CommandError(str(e))
|
|
513
|
+
|
|
514
|
+
self.require_node(node, "feature", self.feature_id)
|
|
515
|
+
|
|
516
|
+
from htmlgraph.cli.base import TextOutputBuilder
|
|
517
|
+
|
|
518
|
+
output = TextOutputBuilder()
|
|
519
|
+
output.add_success(f"Released: {node.id}")
|
|
520
|
+
|
|
521
|
+
return CommandResult(
|
|
522
|
+
data=node_to_dict(node),
|
|
523
|
+
text=output.build(),
|
|
524
|
+
json_data=node_to_dict(node),
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
class FeaturePrimaryCommand(BaseCommand):
|
|
529
|
+
"""Set primary feature."""
|
|
530
|
+
|
|
531
|
+
def __init__(self, *, feature_id: str, collection: str) -> None:
|
|
532
|
+
super().__init__()
|
|
533
|
+
self.feature_id = feature_id
|
|
534
|
+
self.collection = collection
|
|
535
|
+
|
|
536
|
+
@classmethod
|
|
537
|
+
def from_args(cls, args: argparse.Namespace) -> FeaturePrimaryCommand:
|
|
538
|
+
return cls(feature_id=args.id, collection=args.collection)
|
|
539
|
+
|
|
540
|
+
def execute(self) -> CommandResult:
|
|
541
|
+
"""Set primary feature."""
|
|
542
|
+
from htmlgraph.converter import node_to_dict
|
|
543
|
+
|
|
544
|
+
sdk = self.get_sdk()
|
|
545
|
+
|
|
546
|
+
# Only FeatureCollection has set_primary currently
|
|
547
|
+
if self.collection == "features":
|
|
548
|
+
node = sdk.features.set_primary(self.feature_id)
|
|
549
|
+
else:
|
|
550
|
+
# Fallback to direct session manager
|
|
551
|
+
node = sdk.session_manager.set_primary_feature(
|
|
552
|
+
self.feature_id, collection=self.collection, agent=self.agent
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
self.require_node(node, "feature", self.feature_id)
|
|
556
|
+
assert node is not None # Type narrowing for mypy
|
|
557
|
+
|
|
558
|
+
from htmlgraph.cli.base import TextOutputBuilder
|
|
559
|
+
|
|
560
|
+
output = TextOutputBuilder()
|
|
561
|
+
output.add_success(f"Primary feature set: {node.id}")
|
|
562
|
+
output.add_field("Title", node.title)
|
|
563
|
+
|
|
564
|
+
return CommandResult(
|
|
565
|
+
data=node_to_dict(node),
|
|
566
|
+
text=output.build(),
|
|
567
|
+
json_data=node_to_dict(node),
|
|
568
|
+
)
|