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,486 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""HtmlGraph CLI - Track management commands."""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Literal, cast
|
|
9
|
+
|
|
10
|
+
from htmlgraph.cli.base import BaseCommand, CommandError, CommandResult
|
|
11
|
+
from htmlgraph.cli.constants import DEFAULT_GRAPH_DIR
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from argparse import _SubParsersAction
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register_track_commands(subparsers: _SubParsersAction) -> None:
|
|
18
|
+
"""Register track management commands."""
|
|
19
|
+
track_parser = subparsers.add_parser("track", help="Track management")
|
|
20
|
+
track_subparsers = track_parser.add_subparsers(
|
|
21
|
+
dest="track_command", help="Track command"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# track new
|
|
25
|
+
track_new = track_subparsers.add_parser("new", help="Create a new track")
|
|
26
|
+
track_new.add_argument("title", help="Track title")
|
|
27
|
+
track_new.add_argument("--description", help="Track description")
|
|
28
|
+
track_new.add_argument(
|
|
29
|
+
"--priority", choices=["low", "medium", "high"], default="medium"
|
|
30
|
+
)
|
|
31
|
+
track_new.add_argument(
|
|
32
|
+
"--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
|
|
33
|
+
)
|
|
34
|
+
track_new.add_argument(
|
|
35
|
+
"--format", choices=["json", "text"], default="text", help="Output format"
|
|
36
|
+
)
|
|
37
|
+
track_new.set_defaults(func=TrackNewCommand.from_args)
|
|
38
|
+
|
|
39
|
+
# track list
|
|
40
|
+
track_list = track_subparsers.add_parser("list", help="List all tracks")
|
|
41
|
+
track_list.add_argument(
|
|
42
|
+
"--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
|
|
43
|
+
)
|
|
44
|
+
track_list.add_argument(
|
|
45
|
+
"--format", choices=["json", "text"], default="text", help="Output format"
|
|
46
|
+
)
|
|
47
|
+
track_list.set_defaults(func=TrackListCommand.from_args)
|
|
48
|
+
|
|
49
|
+
# track spec
|
|
50
|
+
track_spec = track_subparsers.add_parser("spec", help="Create track spec")
|
|
51
|
+
track_spec.add_argument("track_id", help="Track ID")
|
|
52
|
+
track_spec.add_argument("title", help="Spec title")
|
|
53
|
+
track_spec.add_argument("--overview", help="Spec overview")
|
|
54
|
+
track_spec.add_argument("--context", help="Spec context")
|
|
55
|
+
track_spec.add_argument("--author", help="Spec author")
|
|
56
|
+
track_spec.add_argument(
|
|
57
|
+
"--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
|
|
58
|
+
)
|
|
59
|
+
track_spec.add_argument(
|
|
60
|
+
"--format", choices=["json", "text"], default="text", help="Output format"
|
|
61
|
+
)
|
|
62
|
+
track_spec.set_defaults(func=TrackSpecCommand.from_args)
|
|
63
|
+
|
|
64
|
+
# track plan
|
|
65
|
+
track_plan = track_subparsers.add_parser("plan", help="Create track plan")
|
|
66
|
+
track_plan.add_argument("track_id", help="Track ID")
|
|
67
|
+
track_plan.add_argument("title", help="Plan title")
|
|
68
|
+
track_plan.add_argument(
|
|
69
|
+
"--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
|
|
70
|
+
)
|
|
71
|
+
track_plan.add_argument(
|
|
72
|
+
"--format", choices=["json", "text"], default="text", help="Output format"
|
|
73
|
+
)
|
|
74
|
+
track_plan.set_defaults(func=TrackPlanCommand.from_args)
|
|
75
|
+
|
|
76
|
+
# track delete
|
|
77
|
+
track_delete = track_subparsers.add_parser("delete", help="Delete a track")
|
|
78
|
+
track_delete.add_argument("track_id", help="Track ID")
|
|
79
|
+
track_delete.add_argument(
|
|
80
|
+
"--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
|
|
81
|
+
)
|
|
82
|
+
track_delete.add_argument(
|
|
83
|
+
"--format", choices=["json", "text"], default="text", help="Output format"
|
|
84
|
+
)
|
|
85
|
+
track_delete.set_defaults(func=TrackDeleteCommand.from_args)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ============================================================================
|
|
89
|
+
# Track Commands
|
|
90
|
+
# ============================================================================
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TrackNewCommand(BaseCommand):
|
|
94
|
+
"""Create a new track."""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
title: str,
|
|
100
|
+
description: str | None,
|
|
101
|
+
priority: str,
|
|
102
|
+
) -> None:
|
|
103
|
+
super().__init__()
|
|
104
|
+
self.title = title
|
|
105
|
+
self.description = description
|
|
106
|
+
self.priority = priority
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def from_args(cls, args: argparse.Namespace) -> TrackNewCommand:
|
|
110
|
+
return cls(
|
|
111
|
+
title=args.title, description=args.description, priority=args.priority
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def execute(self) -> CommandResult:
|
|
115
|
+
"""Create a new track."""
|
|
116
|
+
from htmlgraph.track_manager import TrackManager
|
|
117
|
+
|
|
118
|
+
if self.graph_dir is None:
|
|
119
|
+
raise CommandError("Missing graph directory")
|
|
120
|
+
|
|
121
|
+
manager = TrackManager(self.graph_dir)
|
|
122
|
+
|
|
123
|
+
# Type cast priority to expected literal type
|
|
124
|
+
priority_typed = cast(
|
|
125
|
+
Literal["low", "medium", "high", "critical"],
|
|
126
|
+
self.priority,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
track = manager.create_track(
|
|
131
|
+
title=self.title,
|
|
132
|
+
description=self.description or "",
|
|
133
|
+
priority=priority_typed,
|
|
134
|
+
)
|
|
135
|
+
except ValueError as e:
|
|
136
|
+
raise CommandError(str(e))
|
|
137
|
+
|
|
138
|
+
from htmlgraph.cli.base import TextOutputBuilder
|
|
139
|
+
|
|
140
|
+
output = TextOutputBuilder()
|
|
141
|
+
output.add_success(f"Created track: {track.id}")
|
|
142
|
+
output.add_field("Title", track.title)
|
|
143
|
+
output.add_field("Status", track.status)
|
|
144
|
+
output.add_field("Priority", track.priority)
|
|
145
|
+
output.add_field("Path", f"{self.graph_dir}/tracks/{track.id}/")
|
|
146
|
+
output.add_blank()
|
|
147
|
+
output.add_line("Next steps:")
|
|
148
|
+
output.add_field(
|
|
149
|
+
"- Create spec", f"htmlgraph track spec {track.id} 'Spec Title'"
|
|
150
|
+
)
|
|
151
|
+
output.add_field(
|
|
152
|
+
"- Create plan", f"htmlgraph track plan {track.id} 'Plan Title'"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
json_data = {
|
|
156
|
+
"id": track.id,
|
|
157
|
+
"title": track.title,
|
|
158
|
+
"status": track.status,
|
|
159
|
+
"priority": track.priority,
|
|
160
|
+
"path": f"{self.graph_dir}/tracks/{track.id}/",
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return CommandResult(
|
|
164
|
+
data=track,
|
|
165
|
+
text=output.build(),
|
|
166
|
+
json_data=json_data,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TrackListCommand(BaseCommand):
|
|
171
|
+
"""List all tracks."""
|
|
172
|
+
|
|
173
|
+
def __init__(
|
|
174
|
+
self,
|
|
175
|
+
*,
|
|
176
|
+
status: str | None = None,
|
|
177
|
+
priority: str | None = None,
|
|
178
|
+
has_spec: bool | None = None,
|
|
179
|
+
has_plan: bool | None = None,
|
|
180
|
+
) -> None:
|
|
181
|
+
super().__init__()
|
|
182
|
+
self.status = status
|
|
183
|
+
self.priority = priority
|
|
184
|
+
self.has_spec = has_spec
|
|
185
|
+
self.has_plan = has_plan
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def from_args(cls, args: argparse.Namespace) -> TrackListCommand:
|
|
189
|
+
# Validate inputs using TrackFilter model
|
|
190
|
+
from htmlgraph.cli.models import TrackFilter
|
|
191
|
+
|
|
192
|
+
# Get optional filter arguments
|
|
193
|
+
status = getattr(args, "status", None)
|
|
194
|
+
priority = getattr(args, "priority", None)
|
|
195
|
+
has_spec = getattr(args, "has_spec", None)
|
|
196
|
+
has_plan = getattr(args, "has_plan", None)
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
filter_model = TrackFilter(
|
|
200
|
+
status=status, priority=priority, has_spec=has_spec, has_plan=has_plan
|
|
201
|
+
)
|
|
202
|
+
except ValueError as e:
|
|
203
|
+
raise CommandError(str(e))
|
|
204
|
+
|
|
205
|
+
return cls(
|
|
206
|
+
status=filter_model.status,
|
|
207
|
+
priority=filter_model.priority,
|
|
208
|
+
has_spec=filter_model.has_spec,
|
|
209
|
+
has_plan=filter_model.has_plan,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def execute(self) -> CommandResult:
|
|
213
|
+
"""List all tracks."""
|
|
214
|
+
from htmlgraph.track_manager import TrackManager
|
|
215
|
+
|
|
216
|
+
if self.graph_dir is None:
|
|
217
|
+
raise CommandError("Missing graph directory")
|
|
218
|
+
|
|
219
|
+
manager = TrackManager(self.graph_dir)
|
|
220
|
+
track_ids = manager.list_tracks()
|
|
221
|
+
|
|
222
|
+
if not track_ids:
|
|
223
|
+
from htmlgraph.cli.base import TextOutputBuilder
|
|
224
|
+
|
|
225
|
+
output = TextOutputBuilder()
|
|
226
|
+
output.add_warning("No tracks found.")
|
|
227
|
+
output.add_blank()
|
|
228
|
+
output.add_dim("Create a track with: htmlgraph track new 'Track Title'")
|
|
229
|
+
|
|
230
|
+
return CommandResult(
|
|
231
|
+
text=output.build(),
|
|
232
|
+
json_data={"tracks": []},
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Create Rich table
|
|
236
|
+
from htmlgraph.cli.base import TableBuilder
|
|
237
|
+
|
|
238
|
+
builder = TableBuilder.create_list_table(f"Tracks in {self.graph_dir}/tracks/")
|
|
239
|
+
builder.add_id_column("Track ID", no_wrap=True)
|
|
240
|
+
builder.add_column("Components", style="green")
|
|
241
|
+
builder.add_column("Format", style="blue")
|
|
242
|
+
|
|
243
|
+
# Convert to display models for type-safe filtering
|
|
244
|
+
from htmlgraph.cli.models import TrackDisplay
|
|
245
|
+
|
|
246
|
+
display_tracks = []
|
|
247
|
+
|
|
248
|
+
for track_id in track_ids:
|
|
249
|
+
# Check for both consolidated and directory-based formats
|
|
250
|
+
track_file = Path(self.graph_dir) / "tracks" / f"{track_id}.html"
|
|
251
|
+
track_dir = Path(self.graph_dir) / "tracks" / track_id
|
|
252
|
+
|
|
253
|
+
if track_file.exists():
|
|
254
|
+
# Consolidated format
|
|
255
|
+
content = track_file.read_text(encoding="utf-8")
|
|
256
|
+
has_spec = (
|
|
257
|
+
'data-section="overview"' in content
|
|
258
|
+
or 'data-section="requirements"' in content
|
|
259
|
+
)
|
|
260
|
+
has_plan = 'data-section="plan"' in content
|
|
261
|
+
format_type = "consolidated"
|
|
262
|
+
else:
|
|
263
|
+
# Directory format
|
|
264
|
+
has_spec = (track_dir / "spec.html").exists()
|
|
265
|
+
has_plan = (track_dir / "plan.html").exists()
|
|
266
|
+
format_type = "directory"
|
|
267
|
+
|
|
268
|
+
# Create display model
|
|
269
|
+
track_display = TrackDisplay.from_track_id(
|
|
270
|
+
track_id=track_id,
|
|
271
|
+
has_spec=has_spec,
|
|
272
|
+
has_plan=has_plan,
|
|
273
|
+
format_type=format_type,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Apply filters
|
|
277
|
+
if self.has_spec is not None and track_display.has_spec != self.has_spec:
|
|
278
|
+
continue
|
|
279
|
+
if self.has_plan is not None and track_display.has_plan != self.has_plan:
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
display_tracks.append(track_display)
|
|
283
|
+
|
|
284
|
+
for track in display_tracks:
|
|
285
|
+
builder.add_row(track.id, track.components_str, track.format_type)
|
|
286
|
+
|
|
287
|
+
# Return table object directly - TextFormatter will print it properly
|
|
288
|
+
return CommandResult(
|
|
289
|
+
data=builder.table,
|
|
290
|
+
json_data={"tracks": track_ids},
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class TrackSpecCommand(BaseCommand):
|
|
295
|
+
"""Create track spec."""
|
|
296
|
+
|
|
297
|
+
def __init__(
|
|
298
|
+
self,
|
|
299
|
+
*,
|
|
300
|
+
track_id: str,
|
|
301
|
+
title: str,
|
|
302
|
+
overview: str | None,
|
|
303
|
+
context: str | None,
|
|
304
|
+
author: str | None,
|
|
305
|
+
) -> None:
|
|
306
|
+
super().__init__()
|
|
307
|
+
self.track_id = track_id
|
|
308
|
+
self.title = title
|
|
309
|
+
self.overview = overview
|
|
310
|
+
self.context = context
|
|
311
|
+
self.author = author
|
|
312
|
+
|
|
313
|
+
@classmethod
|
|
314
|
+
def from_args(cls, args: argparse.Namespace) -> TrackSpecCommand:
|
|
315
|
+
return cls(
|
|
316
|
+
track_id=args.track_id,
|
|
317
|
+
title=args.title,
|
|
318
|
+
overview=args.overview,
|
|
319
|
+
context=args.context,
|
|
320
|
+
author=args.author,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def execute(self) -> CommandResult:
|
|
324
|
+
"""Create track spec."""
|
|
325
|
+
from htmlgraph.track_manager import TrackManager
|
|
326
|
+
|
|
327
|
+
if self.graph_dir is None:
|
|
328
|
+
raise CommandError("Missing graph directory")
|
|
329
|
+
|
|
330
|
+
manager = TrackManager(self.graph_dir)
|
|
331
|
+
|
|
332
|
+
# Check if track uses consolidated format
|
|
333
|
+
if manager.is_consolidated(self.track_id):
|
|
334
|
+
track_file = manager.tracks_dir / f"{self.track_id}.html"
|
|
335
|
+
msg = [
|
|
336
|
+
f"Track '{self.track_id}' uses consolidated single-file format.",
|
|
337
|
+
f"Spec is embedded in: {track_file}",
|
|
338
|
+
"\nTo create a track with separate spec/plan files, use:",
|
|
339
|
+
' sdk.tracks.builder().separate_files().title("...").create()',
|
|
340
|
+
]
|
|
341
|
+
return CommandResult(text="\n".join(msg))
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
spec = manager.create_spec(
|
|
345
|
+
track_id=self.track_id,
|
|
346
|
+
title=self.title,
|
|
347
|
+
overview=self.overview or "",
|
|
348
|
+
context=self.context or "",
|
|
349
|
+
author=self.author or "",
|
|
350
|
+
)
|
|
351
|
+
except (ValueError, FileNotFoundError) as e:
|
|
352
|
+
raise CommandError(str(e))
|
|
353
|
+
|
|
354
|
+
from htmlgraph.cli.base import TextOutputBuilder
|
|
355
|
+
|
|
356
|
+
output = TextOutputBuilder()
|
|
357
|
+
output.add_success(f"Created spec: {spec.id}")
|
|
358
|
+
output.add_field("Title", spec.title)
|
|
359
|
+
output.add_field("Track", spec.track_id)
|
|
360
|
+
output.add_field("Status", spec.status)
|
|
361
|
+
output.add_field("Path", f"{self.graph_dir}/tracks/{self.track_id}/spec.html")
|
|
362
|
+
output.add_blank()
|
|
363
|
+
output.add_line(
|
|
364
|
+
f"View spec: open {self.graph_dir}/tracks/{self.track_id}/spec.html"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
json_data = {
|
|
368
|
+
"id": spec.id,
|
|
369
|
+
"title": spec.title,
|
|
370
|
+
"track_id": spec.track_id,
|
|
371
|
+
"status": spec.status,
|
|
372
|
+
"path": f"{self.graph_dir}/tracks/{self.track_id}/spec.html",
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return CommandResult(
|
|
376
|
+
data=spec,
|
|
377
|
+
text=output.build(),
|
|
378
|
+
json_data=json_data,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class TrackPlanCommand(BaseCommand):
|
|
383
|
+
"""Create track plan."""
|
|
384
|
+
|
|
385
|
+
def __init__(self, *, track_id: str, title: str) -> None:
|
|
386
|
+
super().__init__()
|
|
387
|
+
self.track_id = track_id
|
|
388
|
+
self.title = title
|
|
389
|
+
|
|
390
|
+
@classmethod
|
|
391
|
+
def from_args(cls, args: argparse.Namespace) -> TrackPlanCommand:
|
|
392
|
+
return cls(track_id=args.track_id, title=args.title)
|
|
393
|
+
|
|
394
|
+
def execute(self) -> CommandResult:
|
|
395
|
+
"""Create track plan."""
|
|
396
|
+
from htmlgraph.track_manager import TrackManager
|
|
397
|
+
|
|
398
|
+
if self.graph_dir is None:
|
|
399
|
+
raise CommandError("Missing graph directory")
|
|
400
|
+
|
|
401
|
+
manager = TrackManager(self.graph_dir)
|
|
402
|
+
|
|
403
|
+
# Check if track uses consolidated format
|
|
404
|
+
if manager.is_consolidated(self.track_id):
|
|
405
|
+
track_file = manager.tracks_dir / f"{self.track_id}.html"
|
|
406
|
+
msg = [
|
|
407
|
+
f"Track '{self.track_id}' uses consolidated single-file format.",
|
|
408
|
+
f"Plan is embedded in: {track_file}",
|
|
409
|
+
"\nTo create a track with separate spec/plan files, use:",
|
|
410
|
+
' sdk.tracks.builder().separate_files().title("...").create()',
|
|
411
|
+
]
|
|
412
|
+
return CommandResult(text="\n".join(msg))
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
plan = manager.create_plan(
|
|
416
|
+
track_id=self.track_id,
|
|
417
|
+
title=self.title,
|
|
418
|
+
)
|
|
419
|
+
except (ValueError, FileNotFoundError) as e:
|
|
420
|
+
raise CommandError(str(e))
|
|
421
|
+
|
|
422
|
+
from htmlgraph.cli.base import TextOutputBuilder
|
|
423
|
+
|
|
424
|
+
output = TextOutputBuilder()
|
|
425
|
+
output.add_success(f"Created plan: {plan.id}")
|
|
426
|
+
output.add_field("Title", plan.title)
|
|
427
|
+
output.add_field("Track", plan.track_id)
|
|
428
|
+
output.add_field("Status", plan.status)
|
|
429
|
+
output.add_field("Path", f"{self.graph_dir}/tracks/{self.track_id}/plan.html")
|
|
430
|
+
output.add_blank()
|
|
431
|
+
output.add_line(
|
|
432
|
+
f"View plan: open {self.graph_dir}/tracks/{self.track_id}/plan.html"
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
json_data = {
|
|
436
|
+
"id": plan.id,
|
|
437
|
+
"title": plan.title,
|
|
438
|
+
"track_id": plan.track_id,
|
|
439
|
+
"status": plan.status,
|
|
440
|
+
"path": f"{self.graph_dir}/tracks/{self.track_id}/plan.html",
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return CommandResult(
|
|
444
|
+
data=plan,
|
|
445
|
+
text=output.build(),
|
|
446
|
+
json_data=json_data,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class TrackDeleteCommand(BaseCommand):
|
|
451
|
+
"""Delete a track."""
|
|
452
|
+
|
|
453
|
+
def __init__(self, *, track_id: str) -> None:
|
|
454
|
+
super().__init__()
|
|
455
|
+
self.track_id = track_id
|
|
456
|
+
|
|
457
|
+
@classmethod
|
|
458
|
+
def from_args(cls, args: argparse.Namespace) -> TrackDeleteCommand:
|
|
459
|
+
return cls(track_id=args.track_id)
|
|
460
|
+
|
|
461
|
+
def execute(self) -> CommandResult:
|
|
462
|
+
"""Delete a track."""
|
|
463
|
+
from htmlgraph.track_manager import TrackManager
|
|
464
|
+
|
|
465
|
+
if self.graph_dir is None:
|
|
466
|
+
raise CommandError("Missing graph directory")
|
|
467
|
+
|
|
468
|
+
manager = TrackManager(self.graph_dir)
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
manager.delete_track(self.track_id)
|
|
472
|
+
except ValueError as e:
|
|
473
|
+
raise CommandError(str(e))
|
|
474
|
+
|
|
475
|
+
from htmlgraph.cli.base import TextOutputBuilder
|
|
476
|
+
|
|
477
|
+
output = TextOutputBuilder()
|
|
478
|
+
output.add_success(f"Deleted track: {self.track_id}")
|
|
479
|
+
output.add_field("Removed", f"{self.graph_dir}/tracks/{self.track_id}/")
|
|
480
|
+
|
|
481
|
+
json_data = {"deleted": True, "track_id": self.track_id}
|
|
482
|
+
|
|
483
|
+
return CommandResult(
|
|
484
|
+
text=output.build(),
|
|
485
|
+
json_data=json_data,
|
|
486
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command implementations."""
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.prompt import Prompt
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from htmlgraph.cli_framework import BaseCommand, CommandError, CommandResult
|
|
11
|
+
|
|
12
|
+
_console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FeatureCreateCommand(BaseCommand):
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
collection: str,
|
|
20
|
+
title: str,
|
|
21
|
+
description: str,
|
|
22
|
+
priority: str,
|
|
23
|
+
steps: Iterable[str] | None,
|
|
24
|
+
track_id: str | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
super().__init__()
|
|
27
|
+
self.collection = collection
|
|
28
|
+
self.title = title
|
|
29
|
+
self.description = description
|
|
30
|
+
self.priority = priority
|
|
31
|
+
self.steps = list(steps) if steps else []
|
|
32
|
+
self.track_id = track_id
|
|
33
|
+
|
|
34
|
+
def execute(self) -> CommandResult:
|
|
35
|
+
sdk = self.get_sdk()
|
|
36
|
+
|
|
37
|
+
# Determine track_id for feature creation
|
|
38
|
+
track_id = self.track_id
|
|
39
|
+
|
|
40
|
+
# Only enforce track selection for main features collection
|
|
41
|
+
if self.collection == "features":
|
|
42
|
+
if not track_id:
|
|
43
|
+
# Get available tracks
|
|
44
|
+
try:
|
|
45
|
+
tracks = sdk.tracks.all()
|
|
46
|
+
if not tracks:
|
|
47
|
+
raise CommandError(
|
|
48
|
+
"No tracks found. Create a track first:\n"
|
|
49
|
+
" uv run htmlgraph track new 'Track Title'"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if len(tracks) == 1:
|
|
53
|
+
# Auto-select if only one track exists
|
|
54
|
+
track_id = tracks[0].id
|
|
55
|
+
_console.print(
|
|
56
|
+
f"[dim]Auto-selected track: {tracks[0].title}[/dim]"
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
# Interactive selection
|
|
60
|
+
_console.print("[bold]Available Tracks:[/bold]")
|
|
61
|
+
for i, track in enumerate(tracks, 1):
|
|
62
|
+
_console.print(f" {i}. {track.title} ({track.id})")
|
|
63
|
+
|
|
64
|
+
selection = Prompt.ask(
|
|
65
|
+
"Select track",
|
|
66
|
+
choices=[str(i) for i in range(1, len(tracks) + 1)],
|
|
67
|
+
)
|
|
68
|
+
track_id = tracks[int(selection) - 1].id
|
|
69
|
+
except Exception as e:
|
|
70
|
+
raise CommandError(f"Failed to get available tracks: {e}")
|
|
71
|
+
|
|
72
|
+
builder = sdk.features.create(
|
|
73
|
+
title=self.title,
|
|
74
|
+
description=self.description,
|
|
75
|
+
priority=self.priority,
|
|
76
|
+
)
|
|
77
|
+
if self.steps:
|
|
78
|
+
builder.add_steps(self.steps)
|
|
79
|
+
if track_id:
|
|
80
|
+
builder.set_track(track_id)
|
|
81
|
+
node = builder.save()
|
|
82
|
+
else:
|
|
83
|
+
node = sdk.session_manager.create_feature(
|
|
84
|
+
title=self.title,
|
|
85
|
+
collection=self.collection,
|
|
86
|
+
description=self.description,
|
|
87
|
+
priority=self.priority,
|
|
88
|
+
steps=self.steps,
|
|
89
|
+
agent=self.agent,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Format output with Rich
|
|
93
|
+
table = Table(show_header=False, box=None)
|
|
94
|
+
table.add_column(style="bold cyan")
|
|
95
|
+
table.add_column()
|
|
96
|
+
|
|
97
|
+
table.add_row("Created:", f"[green]{node.id}[/green]")
|
|
98
|
+
table.add_row("Title:", f"[yellow]{node.title}[/yellow]")
|
|
99
|
+
table.add_row("Status:", f"[blue]{node.status}[/blue]")
|
|
100
|
+
if node.track_id:
|
|
101
|
+
table.add_row("Track:", f"[cyan]{node.track_id}[/cyan]")
|
|
102
|
+
table.add_row(
|
|
103
|
+
"Path:", f"[dim]{self.graph_dir}/{self.collection}/{node.id}.html[/dim]"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Format as Rich panel for text output
|
|
107
|
+
text = [
|
|
108
|
+
f"Created: {node.id}",
|
|
109
|
+
f" Title: {node.title}",
|
|
110
|
+
f" Status: {node.status}",
|
|
111
|
+
]
|
|
112
|
+
if node.track_id:
|
|
113
|
+
text.append(f" Track: {node.track_id}")
|
|
114
|
+
text.append(f" Path: {self.graph_dir}/{self.collection}/{node.id}.html")
|
|
115
|
+
|
|
116
|
+
return CommandResult(data=node, text=text)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class FeatureStartCommand(BaseCommand):
|
|
120
|
+
def __init__(self, *, collection: str, feature_id: str) -> None:
|
|
121
|
+
super().__init__()
|
|
122
|
+
self.collection = collection
|
|
123
|
+
self.feature_id = feature_id
|
|
124
|
+
|
|
125
|
+
def execute(self) -> CommandResult:
|
|
126
|
+
sdk = self.get_sdk()
|
|
127
|
+
collection = getattr(sdk, self.collection, None)
|
|
128
|
+
|
|
129
|
+
if not collection:
|
|
130
|
+
raise CommandError(f"Collection '{self.collection}' not found in SDK.")
|
|
131
|
+
|
|
132
|
+
node = collection.start(self.feature_id)
|
|
133
|
+
if node is None:
|
|
134
|
+
raise CommandError(
|
|
135
|
+
f"Feature '{self.feature_id}' not found in {self.collection}."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
status = sdk.session_manager.get_status()
|
|
139
|
+
|
|
140
|
+
# Format output with Rich
|
|
141
|
+
table = Table(show_header=False, box=None)
|
|
142
|
+
table.add_column(style="bold cyan")
|
|
143
|
+
table.add_column()
|
|
144
|
+
|
|
145
|
+
table.add_row("Started:", f"[green]{node.id}[/green]")
|
|
146
|
+
table.add_row("Title:", f"[yellow]{node.title}[/yellow]")
|
|
147
|
+
table.add_row("Status:", f"[blue]{node.status}[/blue]")
|
|
148
|
+
wip_color = "red" if status["wip_count"] >= status["wip_limit"] else "green"
|
|
149
|
+
table.add_row(
|
|
150
|
+
"WIP:",
|
|
151
|
+
f"[{wip_color}]{status['wip_count']}/{status['wip_limit']}[/{wip_color}]",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
text = [
|
|
155
|
+
f"Started: {node.id}",
|
|
156
|
+
f" Title: {node.title}",
|
|
157
|
+
f" Status: {node.status}",
|
|
158
|
+
f" WIP: {status['wip_count']}/{status['wip_limit']}",
|
|
159
|
+
]
|
|
160
|
+
return CommandResult(data=node, text=text)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class FeatureCompleteCommand(BaseCommand):
|
|
164
|
+
def __init__(self, *, collection: str, feature_id: str) -> None:
|
|
165
|
+
super().__init__()
|
|
166
|
+
self.collection = collection
|
|
167
|
+
self.feature_id = feature_id
|
|
168
|
+
|
|
169
|
+
def execute(self) -> CommandResult:
|
|
170
|
+
sdk = self.get_sdk()
|
|
171
|
+
collection = getattr(sdk, self.collection, None)
|
|
172
|
+
|
|
173
|
+
if not collection:
|
|
174
|
+
raise CommandError(f"Collection '{self.collection}' not found in SDK.")
|
|
175
|
+
|
|
176
|
+
node = collection.complete(self.feature_id)
|
|
177
|
+
if node is None:
|
|
178
|
+
raise CommandError(
|
|
179
|
+
f"Feature '{self.feature_id}' not found in {self.collection}."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Format output with Rich
|
|
183
|
+
panel = Panel(
|
|
184
|
+
f"[bold green]✓ Completed[/bold green]\n"
|
|
185
|
+
f"[cyan]{node.id}[/cyan]\n"
|
|
186
|
+
f"[yellow]{node.title}[/yellow]",
|
|
187
|
+
border_style="green",
|
|
188
|
+
)
|
|
189
|
+
_console.print(panel)
|
|
190
|
+
|
|
191
|
+
text = [
|
|
192
|
+
f"Completed: {node.id}",
|
|
193
|
+
f" Title: {node.title}",
|
|
194
|
+
]
|
|
195
|
+
return CommandResult(data=node, text=text)
|