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
htmlgraph/cli/base.py
ADDED
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Base classes and utilities for CLI commands.
|
|
4
|
+
|
|
5
|
+
Provides:
|
|
6
|
+
- BaseCommand: Abstract base class for all commands
|
|
7
|
+
- CommandResult: Structured command output
|
|
8
|
+
- CommandError: User-facing errors
|
|
9
|
+
- Formatters: JSON and text output formatting
|
|
10
|
+
- TableBuilder: Utility for creating Rich tables with consistent styling
|
|
11
|
+
- TextOutputBuilder: Utility for building formatted text output consistently
|
|
12
|
+
- save_traceback: Save full tracebacks to log files instead of console
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
import traceback
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
from collections.abc import Iterable
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from datetime import date, datetime
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import TYPE_CHECKING, Any, Literal, Protocol
|
|
26
|
+
|
|
27
|
+
from rich import box
|
|
28
|
+
from rich.console import Console
|
|
29
|
+
from rich.table import Table
|
|
30
|
+
from typing_extensions import Self
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from htmlgraph.sdk import SDK
|
|
34
|
+
|
|
35
|
+
_console = Console()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CommandError(Exception):
|
|
39
|
+
"""User-facing CLI error with an exit code."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, message: str, exit_code: int = 1) -> None:
|
|
42
|
+
super().__init__(message)
|
|
43
|
+
self.exit_code = exit_code
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ============================================================================
|
|
47
|
+
# Traceback Logger - Save error tracebacks to log files
|
|
48
|
+
# ============================================================================
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def save_traceback(error: Exception, context: dict[str, Any] | None = None) -> Path:
|
|
52
|
+
"""Save full traceback to log file instead of printing to console.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
error: The exception that was raised
|
|
56
|
+
context: Optional context dict with command, args, cwd, etc.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Path to the saved log file
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
try:
|
|
63
|
+
# Some operation
|
|
64
|
+
pass
|
|
65
|
+
except Exception as e:
|
|
66
|
+
log_file = save_traceback(e, context={"command": "serve", "cwd": os.getcwd()})
|
|
67
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
68
|
+
console.print(f"[dim]Full traceback saved to:[/dim] {log_file}")
|
|
69
|
+
"""
|
|
70
|
+
# Create logs directory
|
|
71
|
+
log_dir = Path(".htmlgraph/logs/errors")
|
|
72
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
|
|
74
|
+
# Generate filename with timestamp
|
|
75
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
76
|
+
log_file = log_dir / f"error-{timestamp}.log"
|
|
77
|
+
|
|
78
|
+
# Write traceback with context
|
|
79
|
+
with open(log_file, "w") as f:
|
|
80
|
+
f.write(f"Timestamp: {datetime.now().isoformat()}\n")
|
|
81
|
+
if context:
|
|
82
|
+
f.write(f"Context: {context}\n")
|
|
83
|
+
f.write("\n--- Traceback ---\n")
|
|
84
|
+
traceback.print_exc(file=f)
|
|
85
|
+
|
|
86
|
+
return log_file
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ============================================================================
|
|
90
|
+
# TableBuilder - Consistent table styling across CLI
|
|
91
|
+
# ============================================================================
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TableBuilder:
|
|
95
|
+
"""Builder for creating Rich tables with consistent styling.
|
|
96
|
+
|
|
97
|
+
Provides factory methods for common table patterns and column types.
|
|
98
|
+
Eliminates duplicated table creation code across CLI modules.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
# List table with standard styling
|
|
102
|
+
builder = TableBuilder.create_list_table("Features")
|
|
103
|
+
builder.add_id_column()
|
|
104
|
+
builder.add_text_column("Title", max_width=40)
|
|
105
|
+
builder.add_status_column()
|
|
106
|
+
builder.add_timestamp_column("Updated")
|
|
107
|
+
|
|
108
|
+
# Add rows
|
|
109
|
+
for feature in features:
|
|
110
|
+
builder.add_row(feature.id, feature.title, feature.status, feature.updated)
|
|
111
|
+
|
|
112
|
+
# Access the table
|
|
113
|
+
console.print(builder.table)
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
*,
|
|
119
|
+
title: str | None = None,
|
|
120
|
+
show_header: bool = True,
|
|
121
|
+
header_style: str = "bold magenta",
|
|
122
|
+
box_style: box.Box = box.ROUNDED,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Initialize TableBuilder with styling options.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
title: Table title
|
|
128
|
+
show_header: Show header row
|
|
129
|
+
header_style: Style for header text
|
|
130
|
+
box_style: Box drawing style from rich.box
|
|
131
|
+
"""
|
|
132
|
+
self.table = Table(
|
|
133
|
+
title=title,
|
|
134
|
+
show_header=show_header,
|
|
135
|
+
header_style=header_style,
|
|
136
|
+
box=box_style,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def create_list_table(cls, title: str | None = None) -> TableBuilder:
|
|
141
|
+
"""Create a standard list table with rounded box."""
|
|
142
|
+
return cls(title=title, show_header=True, header_style="bold magenta")
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def create_status_table(cls, title: str | None = None) -> TableBuilder:
|
|
146
|
+
"""Create a key-value status table without header."""
|
|
147
|
+
return cls(title=title, show_header=False, box_style=box.SIMPLE)
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def create_compact_table(cls) -> TableBuilder:
|
|
151
|
+
"""Create a compact table with no header or box."""
|
|
152
|
+
return cls(title=None, show_header=False, box_style=box.SIMPLE)
|
|
153
|
+
|
|
154
|
+
def add_id_column(
|
|
155
|
+
self,
|
|
156
|
+
name: str = "ID",
|
|
157
|
+
*,
|
|
158
|
+
style: str = "cyan",
|
|
159
|
+
no_wrap: bool = False,
|
|
160
|
+
max_width: int | None = None,
|
|
161
|
+
) -> TableBuilder:
|
|
162
|
+
"""Add an ID column with cyan styling.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
name: Column header name
|
|
166
|
+
style: Text style
|
|
167
|
+
no_wrap: Prevent text wrapping
|
|
168
|
+
max_width: Maximum column width in characters
|
|
169
|
+
"""
|
|
170
|
+
self.table.add_column(name, style=style, no_wrap=no_wrap, max_width=max_width)
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
def add_text_column(
|
|
174
|
+
self,
|
|
175
|
+
name: str,
|
|
176
|
+
*,
|
|
177
|
+
style: str = "yellow",
|
|
178
|
+
max_width: int | None = None,
|
|
179
|
+
no_wrap: bool = False,
|
|
180
|
+
) -> TableBuilder:
|
|
181
|
+
"""Add a text column with yellow styling.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
name: Column header name
|
|
185
|
+
style: Text style
|
|
186
|
+
max_width: Maximum column width in characters
|
|
187
|
+
no_wrap: Prevent text wrapping
|
|
188
|
+
"""
|
|
189
|
+
self.table.add_column(name, style=style, max_width=max_width, no_wrap=no_wrap)
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
def add_status_column(
|
|
193
|
+
self,
|
|
194
|
+
name: str = "Status",
|
|
195
|
+
*,
|
|
196
|
+
style: str = "green",
|
|
197
|
+
width: int | None = None,
|
|
198
|
+
) -> TableBuilder:
|
|
199
|
+
"""Add a status column with green styling.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
name: Column header name
|
|
203
|
+
style: Text style
|
|
204
|
+
width: Fixed column width in characters
|
|
205
|
+
"""
|
|
206
|
+
self.table.add_column(name, style=style, width=width)
|
|
207
|
+
return self
|
|
208
|
+
|
|
209
|
+
def add_priority_column(
|
|
210
|
+
self,
|
|
211
|
+
name: str = "Priority",
|
|
212
|
+
*,
|
|
213
|
+
style: str = "blue",
|
|
214
|
+
width: int | None = None,
|
|
215
|
+
) -> TableBuilder:
|
|
216
|
+
"""Add a priority column with blue styling.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
name: Column header name
|
|
220
|
+
style: Text style
|
|
221
|
+
width: Fixed column width in characters
|
|
222
|
+
"""
|
|
223
|
+
self.table.add_column(name, style=style, width=width)
|
|
224
|
+
return self
|
|
225
|
+
|
|
226
|
+
def add_timestamp_column(
|
|
227
|
+
self,
|
|
228
|
+
name: str,
|
|
229
|
+
*,
|
|
230
|
+
style: str = "white",
|
|
231
|
+
width: int | None = None,
|
|
232
|
+
) -> TableBuilder:
|
|
233
|
+
"""Add a timestamp column with white styling.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
name: Column header name
|
|
237
|
+
style: Text style
|
|
238
|
+
width: Fixed column width in characters
|
|
239
|
+
"""
|
|
240
|
+
self.table.add_column(name, style=style, width=width)
|
|
241
|
+
return self
|
|
242
|
+
|
|
243
|
+
def add_numeric_column(
|
|
244
|
+
self,
|
|
245
|
+
name: str,
|
|
246
|
+
*,
|
|
247
|
+
style: str = "yellow",
|
|
248
|
+
justify: Literal["left", "center", "right"] = "right",
|
|
249
|
+
width: int | None = None,
|
|
250
|
+
) -> TableBuilder:
|
|
251
|
+
"""Add a numeric column with right justification.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
name: Column header name
|
|
255
|
+
style: Text style
|
|
256
|
+
justify: Text alignment
|
|
257
|
+
width: Fixed column width in characters
|
|
258
|
+
"""
|
|
259
|
+
self.table.add_column(name, style=style, justify=justify, width=width)
|
|
260
|
+
return self
|
|
261
|
+
|
|
262
|
+
def add_column(
|
|
263
|
+
self,
|
|
264
|
+
name: str,
|
|
265
|
+
*,
|
|
266
|
+
style: str | None = None,
|
|
267
|
+
justify: Literal["left", "center", "right"] = "left",
|
|
268
|
+
width: int | None = None,
|
|
269
|
+
max_width: int | None = None,
|
|
270
|
+
no_wrap: bool = False,
|
|
271
|
+
) -> TableBuilder:
|
|
272
|
+
"""Add a custom column with full control over styling.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
name: Column header name
|
|
276
|
+
style: Text style (e.g., "cyan", "bold red")
|
|
277
|
+
justify: Text alignment
|
|
278
|
+
width: Fixed column width in characters
|
|
279
|
+
max_width: Maximum column width in characters
|
|
280
|
+
no_wrap: Prevent text wrapping
|
|
281
|
+
"""
|
|
282
|
+
self.table.add_column(
|
|
283
|
+
name,
|
|
284
|
+
style=style,
|
|
285
|
+
justify=justify,
|
|
286
|
+
width=width,
|
|
287
|
+
max_width=max_width,
|
|
288
|
+
no_wrap=no_wrap,
|
|
289
|
+
)
|
|
290
|
+
return self
|
|
291
|
+
|
|
292
|
+
def add_row(self, *values: str) -> TableBuilder:
|
|
293
|
+
"""Add a data row to the table.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
*values: Cell values (converted to strings)
|
|
297
|
+
"""
|
|
298
|
+
self.table.add_row(*values)
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
def add_separator(self, style: str = "dim") -> TableBuilder:
|
|
302
|
+
"""Add a separator row.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
style: Style for separator row
|
|
306
|
+
"""
|
|
307
|
+
# Add empty row with style
|
|
308
|
+
num_columns = len(self.table.columns)
|
|
309
|
+
self.table.add_row(*[""] * num_columns, style=style)
|
|
310
|
+
return self
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ============================================================================
|
|
314
|
+
# TextOutputBuilder - Consistent text output formatting across CLI
|
|
315
|
+
# ============================================================================
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class TextOutputBuilder:
|
|
319
|
+
"""Builder for creating formatted text output consistently.
|
|
320
|
+
|
|
321
|
+
Provides fluent API methods for building structured text output with
|
|
322
|
+
Rich console styling. Eliminates duplicated text output building code
|
|
323
|
+
across CLI modules.
|
|
324
|
+
|
|
325
|
+
Example:
|
|
326
|
+
output = TextOutputBuilder()
|
|
327
|
+
output.add_success(f"Session started: {session.id}")
|
|
328
|
+
output.add_field("Agent", session.agent)
|
|
329
|
+
output.add_field("Started", session.started_at.isoformat())
|
|
330
|
+
return CommandResult(text=output.build())
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
def __init__(self) -> None:
|
|
334
|
+
"""Initialize TextOutputBuilder with empty lines list."""
|
|
335
|
+
self._lines: list[str] = []
|
|
336
|
+
|
|
337
|
+
def add_success(self, message: str) -> Self:
|
|
338
|
+
"""Add success message with green styling.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
message: Success message text
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Self for method chaining
|
|
345
|
+
"""
|
|
346
|
+
from htmlgraph.cli.constants import get_style
|
|
347
|
+
|
|
348
|
+
self._lines.append(f"{get_style('success')}{message}")
|
|
349
|
+
return self
|
|
350
|
+
|
|
351
|
+
def add_error(self, message: str) -> Self:
|
|
352
|
+
"""Add error message with red styling.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
message: Error message text
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Self for method chaining
|
|
359
|
+
"""
|
|
360
|
+
from htmlgraph.cli.constants import get_style
|
|
361
|
+
|
|
362
|
+
self._lines.append(f"{get_style('error')}{message}")
|
|
363
|
+
return self
|
|
364
|
+
|
|
365
|
+
def add_warning(self, message: str) -> Self:
|
|
366
|
+
"""Add warning message with yellow styling.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
message: Warning message text
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Self for method chaining
|
|
373
|
+
"""
|
|
374
|
+
from htmlgraph.cli.constants import get_style
|
|
375
|
+
|
|
376
|
+
self._lines.append(f"{get_style('warning')}{message}")
|
|
377
|
+
return self
|
|
378
|
+
|
|
379
|
+
def add_info(self, message: str) -> Self:
|
|
380
|
+
"""Add info message with cyan styling.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
message: Info message text
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Self for method chaining
|
|
387
|
+
"""
|
|
388
|
+
from htmlgraph.cli.constants import get_style
|
|
389
|
+
|
|
390
|
+
self._lines.append(f"{get_style('info')}{message}")
|
|
391
|
+
return self
|
|
392
|
+
|
|
393
|
+
def add_dim(self, message: str) -> Self:
|
|
394
|
+
"""Add dimmed message with dim styling.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
message: Dimmed message text
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Self for method chaining
|
|
401
|
+
"""
|
|
402
|
+
from htmlgraph.cli.constants import get_style
|
|
403
|
+
|
|
404
|
+
self._lines.append(f"{get_style('dim')}{message}")
|
|
405
|
+
return self
|
|
406
|
+
|
|
407
|
+
def add_field(self, label: str, value: str | int | float | None) -> Self:
|
|
408
|
+
"""Add indented field in 'Label: value' format.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
label: Field label
|
|
412
|
+
value: Field value (converted to string)
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Self for method chaining
|
|
416
|
+
"""
|
|
417
|
+
value_str = str(value) if value is not None else ""
|
|
418
|
+
self._lines.append(f" {label}: {value_str}")
|
|
419
|
+
return self
|
|
420
|
+
|
|
421
|
+
def add_line(self, text: str) -> Self:
|
|
422
|
+
"""Add plain text line without styling.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
text: Plain text to add
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Self for method chaining
|
|
429
|
+
"""
|
|
430
|
+
self._lines.append(text)
|
|
431
|
+
return self
|
|
432
|
+
|
|
433
|
+
def add_blank(self) -> Self:
|
|
434
|
+
"""Add blank line.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Self for method chaining
|
|
438
|
+
"""
|
|
439
|
+
self._lines.append("")
|
|
440
|
+
return self
|
|
441
|
+
|
|
442
|
+
def build(self) -> str:
|
|
443
|
+
"""Build final text output by joining all lines.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Joined string with newline separators
|
|
447
|
+
"""
|
|
448
|
+
return "\n".join(self._lines)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@dataclass
|
|
452
|
+
class CommandResult:
|
|
453
|
+
"""Structured command result for flexible output formatting."""
|
|
454
|
+
|
|
455
|
+
data: Any = None
|
|
456
|
+
text: str | Iterable[str] | None = None
|
|
457
|
+
json_data: Any | None = None
|
|
458
|
+
exit_code: int = 0 # Exit code for the command (0 = success)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class Formatter(Protocol):
|
|
462
|
+
"""Protocol for output formatters."""
|
|
463
|
+
|
|
464
|
+
def output(self, result: CommandResult) -> None: ...
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _serialize_json(value: Any) -> Any:
|
|
468
|
+
"""Recursively serialize value to JSON-compatible types.
|
|
469
|
+
|
|
470
|
+
Sanitizes strings to remove control characters (newlines, tabs) that
|
|
471
|
+
would break JSON validity when using json.dumps().
|
|
472
|
+
"""
|
|
473
|
+
if value is None:
|
|
474
|
+
return None
|
|
475
|
+
if isinstance(value, (datetime, date)):
|
|
476
|
+
return value.isoformat()
|
|
477
|
+
if isinstance(value, str):
|
|
478
|
+
# Sanitize string: replace control characters with spaces
|
|
479
|
+
# This prevents newlines/tabs in JSON string values from breaking JSON validity
|
|
480
|
+
sanitized = value.replace("\n", " ").replace("\r", " ").replace("\t", " ")
|
|
481
|
+
# Collapse multiple spaces to single space
|
|
482
|
+
sanitized = " ".join(sanitized.split())
|
|
483
|
+
return sanitized
|
|
484
|
+
if hasattr(value, "model_dump") and callable(getattr(value, "model_dump")):
|
|
485
|
+
return _serialize_json(value.model_dump())
|
|
486
|
+
if hasattr(value, "to_dict") and callable(getattr(value, "to_dict")):
|
|
487
|
+
return _serialize_json(value.to_dict())
|
|
488
|
+
if isinstance(value, dict):
|
|
489
|
+
return {key: _serialize_json(val) for key, val in value.items()}
|
|
490
|
+
if isinstance(value, (list, tuple, set)):
|
|
491
|
+
return [_serialize_json(item) for item in value]
|
|
492
|
+
return value
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class JsonFormatter:
|
|
496
|
+
"""Format command output as JSON."""
|
|
497
|
+
|
|
498
|
+
def output(self, result: CommandResult) -> None:
|
|
499
|
+
payload = result.json_data if result.json_data is not None else result.data
|
|
500
|
+
# Use sys.stdout.write instead of _console.print to avoid Rich's line-wrapping
|
|
501
|
+
# which inserts literal newlines into JSON string values, breaking JSON validity
|
|
502
|
+
sys.stdout.write(json.dumps(_serialize_json(payload), indent=2) + "\n")
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
class TextFormatter:
|
|
506
|
+
"""Format command output as plain text."""
|
|
507
|
+
|
|
508
|
+
def output(self, result: CommandResult) -> None:
|
|
509
|
+
# If data is provided and it's a Rich renderable, print it directly
|
|
510
|
+
if result.data is not None:
|
|
511
|
+
from rich.table import Table
|
|
512
|
+
|
|
513
|
+
# Check if data is a Rich renderable (Table, Panel, etc.)
|
|
514
|
+
if isinstance(result.data, (Table,)) or hasattr(result.data, "__rich__"):
|
|
515
|
+
_console.print(result.data)
|
|
516
|
+
return
|
|
517
|
+
|
|
518
|
+
# Fall back to text output
|
|
519
|
+
if result.text is None:
|
|
520
|
+
if result.data is not None:
|
|
521
|
+
_console.print(result.data)
|
|
522
|
+
return
|
|
523
|
+
if isinstance(result.text, str):
|
|
524
|
+
# Use sys.stdout.write() for ANSI-formatted text to preserve colors when piped
|
|
525
|
+
# This bypasses Rich's reprocessing and ensures ANSI codes are preserved
|
|
526
|
+
sys.stdout.write(result.text)
|
|
527
|
+
if not result.text.endswith("\n"):
|
|
528
|
+
sys.stdout.write("\n")
|
|
529
|
+
return
|
|
530
|
+
# For text as list/iterable, write directly to preserve ANSI codes
|
|
531
|
+
sys.stdout.write("\n".join(str(line) for line in result.text) + "\n")
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def get_formatter(format_name: str) -> Formatter:
|
|
535
|
+
"""Get formatter by name (json, text, plain, refs)."""
|
|
536
|
+
if format_name == "json":
|
|
537
|
+
return JsonFormatter()
|
|
538
|
+
if format_name in ("text", "plain", "refs", ""):
|
|
539
|
+
return TextFormatter()
|
|
540
|
+
raise CommandError(f"Unknown output format '{format_name}'")
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
class BaseCommand(ABC):
|
|
544
|
+
"""Abstract base class for all CLI commands.
|
|
545
|
+
|
|
546
|
+
Provides:
|
|
547
|
+
- SDK initialization and caching
|
|
548
|
+
- Structured error handling
|
|
549
|
+
- Validation lifecycle hook
|
|
550
|
+
- Output formatting
|
|
551
|
+
|
|
552
|
+
Subclasses must implement:
|
|
553
|
+
- from_args(): Create command instance from argparse.Namespace
|
|
554
|
+
- execute(): Execute command logic and return CommandResult
|
|
555
|
+
"""
|
|
556
|
+
|
|
557
|
+
def __init__(self) -> None:
|
|
558
|
+
self.graph_dir: str | None = None
|
|
559
|
+
self.agent: str | None = None
|
|
560
|
+
self._sdk: SDK | None = None
|
|
561
|
+
self.override_output_format: str | None = (
|
|
562
|
+
None # Allow commands to override formatter
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
@classmethod
|
|
566
|
+
@abstractmethod
|
|
567
|
+
def from_args(cls, args: argparse.Namespace) -> BaseCommand:
|
|
568
|
+
"""Create command instance from argparse arguments.
|
|
569
|
+
|
|
570
|
+
This separates argument parsing from command execution,
|
|
571
|
+
making commands easier to test.
|
|
572
|
+
"""
|
|
573
|
+
raise NotImplementedError
|
|
574
|
+
|
|
575
|
+
def validate(self) -> None:
|
|
576
|
+
"""Validate command parameters before execution.
|
|
577
|
+
|
|
578
|
+
Raise CommandError if validation fails.
|
|
579
|
+
Default implementation does nothing.
|
|
580
|
+
"""
|
|
581
|
+
return None
|
|
582
|
+
|
|
583
|
+
@abstractmethod
|
|
584
|
+
def execute(self) -> CommandResult:
|
|
585
|
+
"""Execute the command and return structured result.
|
|
586
|
+
|
|
587
|
+
Raise CommandError for user-facing errors.
|
|
588
|
+
"""
|
|
589
|
+
raise NotImplementedError
|
|
590
|
+
|
|
591
|
+
def get_sdk(self) -> SDK:
|
|
592
|
+
"""Get or create SDK instance.
|
|
593
|
+
|
|
594
|
+
Caches SDK to avoid repeated initialization.
|
|
595
|
+
"""
|
|
596
|
+
if self.graph_dir is None:
|
|
597
|
+
raise CommandError("Missing graph directory for command execution.")
|
|
598
|
+
if self._sdk is None:
|
|
599
|
+
from htmlgraph.sdk import SDK
|
|
600
|
+
|
|
601
|
+
self._sdk = SDK(directory=self.graph_dir, agent=self.agent)
|
|
602
|
+
return self._sdk
|
|
603
|
+
|
|
604
|
+
def require_node(self, node: Any, entity_type: str, entity_id: str) -> None:
|
|
605
|
+
"""Validate that a node exists, raising CommandError if None.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
node: The node object to validate
|
|
609
|
+
entity_type: Type of entity (feature, session, track, etc.)
|
|
610
|
+
entity_id: ID of the entity for error message
|
|
611
|
+
|
|
612
|
+
Raises:
|
|
613
|
+
CommandError: If node is None
|
|
614
|
+
|
|
615
|
+
Usage:
|
|
616
|
+
node = collection.get(feature_id)
|
|
617
|
+
self.require_node(node, "feature", feature_id)
|
|
618
|
+
"""
|
|
619
|
+
if node is None:
|
|
620
|
+
from htmlgraph.cli.constants import get_error_message
|
|
621
|
+
|
|
622
|
+
error_key = f"{entity_type}_not_found"
|
|
623
|
+
id_key = f"{entity_type}_id"
|
|
624
|
+
raise CommandError(get_error_message(error_key, **{id_key: entity_id}))
|
|
625
|
+
|
|
626
|
+
def require_value(self, value: Any, message: str) -> None:
|
|
627
|
+
"""Generic validation helper that raises CommandError if value is falsy.
|
|
628
|
+
|
|
629
|
+
Args:
|
|
630
|
+
value: The value to validate
|
|
631
|
+
message: Error message to raise if validation fails
|
|
632
|
+
|
|
633
|
+
Raises:
|
|
634
|
+
CommandError: If value is falsy (None, False, empty string, etc.)
|
|
635
|
+
|
|
636
|
+
Usage:
|
|
637
|
+
self.require_value(self.title, "Title is required")
|
|
638
|
+
self.require_value(len(items) > 0, "At least one item required")
|
|
639
|
+
"""
|
|
640
|
+
if not value:
|
|
641
|
+
raise CommandError(message)
|
|
642
|
+
|
|
643
|
+
def require_collection(self, collection: Any, collection_name: str) -> None:
|
|
644
|
+
"""Validate that a collection exists on SDK, raising CommandError if None.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
collection: The collection object to validate
|
|
648
|
+
collection_name: Name of the collection for error message
|
|
649
|
+
|
|
650
|
+
Raises:
|
|
651
|
+
CommandError: If collection is None/falsy
|
|
652
|
+
|
|
653
|
+
Usage:
|
|
654
|
+
collection = getattr(sdk, self.collection, None)
|
|
655
|
+
self.require_collection(collection, self.collection)
|
|
656
|
+
"""
|
|
657
|
+
if not collection:
|
|
658
|
+
raise CommandError(f"Collection '{collection_name}' not found in SDK")
|
|
659
|
+
|
|
660
|
+
def run(self, *, graph_dir: str, agent: str | None, output_format: str) -> None:
|
|
661
|
+
"""Run command with context.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
graph_dir: Path to .htmlgraph directory
|
|
665
|
+
agent: Agent name (optional)
|
|
666
|
+
output_format: Output format (json, text, plain)
|
|
667
|
+
"""
|
|
668
|
+
self.graph_dir = graph_dir
|
|
669
|
+
self.agent = agent
|
|
670
|
+
try:
|
|
671
|
+
self.validate()
|
|
672
|
+
result = self.execute()
|
|
673
|
+
# Allow commands to override output format
|
|
674
|
+
# (e.g., snapshot command's --output-format flag overrides global --format)
|
|
675
|
+
actual_format = self.override_output_format or output_format
|
|
676
|
+
formatter = get_formatter(actual_format)
|
|
677
|
+
formatter.output(result)
|
|
678
|
+
except CommandError as exc:
|
|
679
|
+
error_console = Console(file=sys.stderr)
|
|
680
|
+
error_console.print(f"[red]Error: {exc}[/red]")
|
|
681
|
+
sys.exit(exc.exit_code)
|
|
682
|
+
except ValueError as exc:
|
|
683
|
+
error_console = Console(file=sys.stderr)
|
|
684
|
+
error_console.print(f"[red]Error: {exc}[/red]")
|
|
685
|
+
sys.exit(1)
|