htmlgraph 0.26.24__py3-none-any.whl → 0.26.25__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/__init__.py +1 -1
- htmlgraph/api/main.py +56 -23
- htmlgraph/api/templates/dashboard-redesign.html +3 -3
- htmlgraph/api/templates/dashboard.html +3 -3
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/builders/track.py +26 -0
- htmlgraph/cli/base.py +31 -7
- htmlgraph/cli/work/__init__.py +74 -0
- htmlgraph/cli/work/browse.py +114 -0
- htmlgraph/cli/work/snapshot.py +558 -0
- htmlgraph/collections/base.py +34 -0
- htmlgraph/collections/todo.py +12 -0
- htmlgraph/converter.py +11 -0
- htmlgraph/hooks/orchestrator.py +88 -14
- htmlgraph/hooks/session_handler.py +3 -1
- htmlgraph/models.py +18 -1
- htmlgraph/orchestration/__init__.py +4 -0
- htmlgraph/orchestration/plugin_manager.py +1 -2
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/refs.py +343 -0
- htmlgraph/sdk.py +71 -1
- htmlgraph/session_manager.py +1 -7
- htmlgraph/sessions/handoff.py +6 -0
- htmlgraph/track_builder.py +12 -0
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.26.25.dist-info}/METADATA +1 -1
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.26.25.dist-info}/RECORD +33 -28
- {htmlgraph-0.26.24.data → htmlgraph-0.26.25.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.26.25.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.26.25.dist-info}/WHEEL +0 -0
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.26.25.dist-info}/entry_points.txt +0 -0
htmlgraph/cli/base.py
CHANGED
|
@@ -464,11 +464,22 @@ class Formatter(Protocol):
|
|
|
464
464
|
|
|
465
465
|
|
|
466
466
|
def _serialize_json(value: Any) -> Any:
|
|
467
|
-
"""Recursively serialize value to JSON-compatible types.
|
|
467
|
+
"""Recursively serialize value to JSON-compatible types.
|
|
468
|
+
|
|
469
|
+
Sanitizes strings to remove control characters (newlines, tabs) that
|
|
470
|
+
would break JSON validity when using json.dumps().
|
|
471
|
+
"""
|
|
468
472
|
if value is None:
|
|
469
473
|
return None
|
|
470
474
|
if isinstance(value, (datetime, date)):
|
|
471
475
|
return value.isoformat()
|
|
476
|
+
if isinstance(value, str):
|
|
477
|
+
# Sanitize string: replace control characters with spaces
|
|
478
|
+
# This prevents newlines/tabs in JSON string values from breaking JSON validity
|
|
479
|
+
sanitized = value.replace("\n", " ").replace("\r", " ").replace("\t", " ")
|
|
480
|
+
# Collapse multiple spaces to single space
|
|
481
|
+
sanitized = " ".join(sanitized.split())
|
|
482
|
+
return sanitized
|
|
472
483
|
if hasattr(value, "model_dump") and callable(getattr(value, "model_dump")):
|
|
473
484
|
return _serialize_json(value.model_dump())
|
|
474
485
|
if hasattr(value, "to_dict") and callable(getattr(value, "to_dict")):
|
|
@@ -485,7 +496,9 @@ class JsonFormatter:
|
|
|
485
496
|
|
|
486
497
|
def output(self, result: CommandResult) -> None:
|
|
487
498
|
payload = result.json_data if result.json_data is not None else result.data
|
|
488
|
-
_console.print
|
|
499
|
+
# Use sys.stdout.write instead of _console.print to avoid Rich's line-wrapping
|
|
500
|
+
# which inserts literal newlines into JSON string values, breaking JSON validity
|
|
501
|
+
sys.stdout.write(json.dumps(_serialize_json(payload), indent=2) + "\n")
|
|
489
502
|
|
|
490
503
|
|
|
491
504
|
class TextFormatter:
|
|
@@ -507,16 +520,21 @@ class TextFormatter:
|
|
|
507
520
|
_console.print(result.data)
|
|
508
521
|
return
|
|
509
522
|
if isinstance(result.text, str):
|
|
510
|
-
|
|
523
|
+
# Use sys.stdout.write() for ANSI-formatted text to preserve colors when piped
|
|
524
|
+
# This bypasses Rich's reprocessing and ensures ANSI codes are preserved
|
|
525
|
+
sys.stdout.write(result.text)
|
|
526
|
+
if not result.text.endswith("\n"):
|
|
527
|
+
sys.stdout.write("\n")
|
|
511
528
|
return
|
|
512
|
-
|
|
529
|
+
# For text as list/iterable, write directly to preserve ANSI codes
|
|
530
|
+
sys.stdout.write("\n".join(str(line) for line in result.text) + "\n")
|
|
513
531
|
|
|
514
532
|
|
|
515
533
|
def get_formatter(format_name: str) -> Formatter:
|
|
516
|
-
"""Get formatter by name (json, text, plain)."""
|
|
534
|
+
"""Get formatter by name (json, text, plain, refs)."""
|
|
517
535
|
if format_name == "json":
|
|
518
536
|
return JsonFormatter()
|
|
519
|
-
if format_name in ("text", "plain", ""):
|
|
537
|
+
if format_name in ("text", "plain", "refs", ""):
|
|
520
538
|
return TextFormatter()
|
|
521
539
|
raise CommandError(f"Unknown output format '{format_name}'")
|
|
522
540
|
|
|
@@ -539,6 +557,9 @@ class BaseCommand(ABC):
|
|
|
539
557
|
self.graph_dir: str | None = None
|
|
540
558
|
self.agent: str | None = None
|
|
541
559
|
self._sdk: SDK | None = None
|
|
560
|
+
self.override_output_format: str | None = (
|
|
561
|
+
None # Allow commands to override formatter
|
|
562
|
+
)
|
|
542
563
|
|
|
543
564
|
@classmethod
|
|
544
565
|
@abstractmethod
|
|
@@ -648,7 +669,10 @@ class BaseCommand(ABC):
|
|
|
648
669
|
try:
|
|
649
670
|
self.validate()
|
|
650
671
|
result = self.execute()
|
|
651
|
-
|
|
672
|
+
# Allow commands to override output format
|
|
673
|
+
# (e.g., snapshot command's --output-format flag overrides global --format)
|
|
674
|
+
actual_format = self.override_output_format or output_format
|
|
675
|
+
formatter = get_formatter(actual_format)
|
|
652
676
|
formatter.output(result)
|
|
653
677
|
except CommandError as exc:
|
|
654
678
|
error_console = Console(file=sys.stderr)
|
htmlgraph/cli/work/__init__.py
CHANGED
|
@@ -23,6 +23,7 @@ def register_commands(subparsers: _SubParsersAction) -> None:
|
|
|
23
23
|
Args:
|
|
24
24
|
subparsers: Subparser action from ArgumentParser.add_subparsers()
|
|
25
25
|
"""
|
|
26
|
+
from htmlgraph.cli.work.browse import BrowseCommand
|
|
26
27
|
from htmlgraph.cli.work.features import register_feature_commands
|
|
27
28
|
from htmlgraph.cli.work.orchestration import (
|
|
28
29
|
register_archive_commands,
|
|
@@ -31,6 +32,7 @@ def register_commands(subparsers: _SubParsersAction) -> None:
|
|
|
31
32
|
)
|
|
32
33
|
from htmlgraph.cli.work.report import register_report_commands
|
|
33
34
|
from htmlgraph.cli.work.sessions import register_session_commands
|
|
35
|
+
from htmlgraph.cli.work.snapshot import SnapshotCommand
|
|
34
36
|
from htmlgraph.cli.work.tracks import register_track_commands
|
|
35
37
|
|
|
36
38
|
# Register all command groups
|
|
@@ -42,8 +44,75 @@ def register_commands(subparsers: _SubParsersAction) -> None:
|
|
|
42
44
|
register_claude_commands(subparsers)
|
|
43
45
|
register_report_commands(subparsers)
|
|
44
46
|
|
|
47
|
+
# Snapshot command
|
|
48
|
+
snapshot_parser = subparsers.add_parser(
|
|
49
|
+
"snapshot",
|
|
50
|
+
help="Output current graph state with refs",
|
|
51
|
+
)
|
|
52
|
+
snapshot_parser.add_argument(
|
|
53
|
+
"--output-format",
|
|
54
|
+
choices=["refs", "json", "text"],
|
|
55
|
+
default="refs",
|
|
56
|
+
help="Output format (default: refs)",
|
|
57
|
+
)
|
|
58
|
+
snapshot_parser.add_argument(
|
|
59
|
+
"--type",
|
|
60
|
+
help="Filter by type (feature, track, bug, spike, chore, epic, all)",
|
|
61
|
+
)
|
|
62
|
+
snapshot_parser.add_argument(
|
|
63
|
+
"--status",
|
|
64
|
+
help="Filter by status (todo, in_progress, blocked, done, all)",
|
|
65
|
+
)
|
|
66
|
+
snapshot_parser.add_argument(
|
|
67
|
+
"--track",
|
|
68
|
+
help="Show only items in a specific track (by track ID or ref)",
|
|
69
|
+
)
|
|
70
|
+
snapshot_parser.add_argument(
|
|
71
|
+
"--active",
|
|
72
|
+
action="store_true",
|
|
73
|
+
help="Show only TODO/IN_PROGRESS items (filters out metadata spikes)",
|
|
74
|
+
)
|
|
75
|
+
snapshot_parser.add_argument(
|
|
76
|
+
"--blockers",
|
|
77
|
+
action="store_true",
|
|
78
|
+
help="Show only critical/blocked items",
|
|
79
|
+
)
|
|
80
|
+
snapshot_parser.add_argument(
|
|
81
|
+
"--summary",
|
|
82
|
+
action="store_true",
|
|
83
|
+
help="Show counts and progress summary instead of listing all items",
|
|
84
|
+
)
|
|
85
|
+
snapshot_parser.add_argument(
|
|
86
|
+
"--my-work",
|
|
87
|
+
action="store_true",
|
|
88
|
+
help="Show items assigned to current agent",
|
|
89
|
+
)
|
|
90
|
+
snapshot_parser.set_defaults(func=SnapshotCommand.from_args)
|
|
91
|
+
|
|
92
|
+
# Browse command
|
|
93
|
+
browse_parser = subparsers.add_parser(
|
|
94
|
+
"browse",
|
|
95
|
+
help="Open dashboard in browser",
|
|
96
|
+
)
|
|
97
|
+
browse_parser.add_argument(
|
|
98
|
+
"--port",
|
|
99
|
+
type=int,
|
|
100
|
+
default=8080,
|
|
101
|
+
help="Server port (default: 8080)",
|
|
102
|
+
)
|
|
103
|
+
browse_parser.add_argument(
|
|
104
|
+
"--query-type",
|
|
105
|
+
help="Filter by type (feature, track, bug, spike, chore, epic)",
|
|
106
|
+
)
|
|
107
|
+
browse_parser.add_argument(
|
|
108
|
+
"--query-status",
|
|
109
|
+
help="Filter by status (todo, in_progress, blocked, done)",
|
|
110
|
+
)
|
|
111
|
+
browse_parser.set_defaults(func=BrowseCommand.from_args)
|
|
112
|
+
|
|
45
113
|
|
|
46
114
|
# Re-export all command classes for backward compatibility
|
|
115
|
+
from htmlgraph.cli.work.browse import BrowseCommand
|
|
47
116
|
from htmlgraph.cli.work.features import (
|
|
48
117
|
FeatureClaimCommand,
|
|
49
118
|
FeatureCompleteCommand,
|
|
@@ -71,6 +140,7 @@ from htmlgraph.cli.work.sessions import (
|
|
|
71
140
|
SessionStartCommand,
|
|
72
141
|
SessionStartInfoCommand,
|
|
73
142
|
)
|
|
143
|
+
from htmlgraph.cli.work.snapshot import SnapshotCommand
|
|
74
144
|
from htmlgraph.cli.work.tracks import (
|
|
75
145
|
TrackDeleteCommand,
|
|
76
146
|
TrackListCommand,
|
|
@@ -89,6 +159,10 @@ __all__ = [
|
|
|
89
159
|
"SessionStartInfoCommand",
|
|
90
160
|
# Report commands
|
|
91
161
|
"SessionReportCommand",
|
|
162
|
+
# Snapshot commands
|
|
163
|
+
"SnapshotCommand",
|
|
164
|
+
# Browse commands
|
|
165
|
+
"BrowseCommand",
|
|
92
166
|
# Feature commands
|
|
93
167
|
"FeatureListCommand",
|
|
94
168
|
"FeatureCreateCommand",
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""HtmlGraph CLI - Browse command for opening dashboard in browser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import webbrowser
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from htmlgraph.cli.base import BaseCommand, CommandResult
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BrowseCommand(BaseCommand):
|
|
16
|
+
"""Open the HtmlGraph dashboard in your default browser.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
htmlgraph browse # Open dashboard
|
|
20
|
+
htmlgraph browse --port 8080 # Custom port
|
|
21
|
+
htmlgraph browse --query-type feature # Show only features
|
|
22
|
+
htmlgraph browse --query-status todo # Show only todo items
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
port: int = 8080,
|
|
29
|
+
query_type: str | None = None,
|
|
30
|
+
query_status: str | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Initialize BrowseCommand.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
port: Server port (default: 8080)
|
|
36
|
+
query_type: Filter by type (feature, track, bug, spike, chore, epic)
|
|
37
|
+
query_status: Filter by status (todo, in_progress, blocked, done)
|
|
38
|
+
"""
|
|
39
|
+
super().__init__()
|
|
40
|
+
self.port = port
|
|
41
|
+
self.query_type = query_type
|
|
42
|
+
self.query_status = query_status
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_args(cls, args: argparse.Namespace) -> BrowseCommand:
|
|
46
|
+
"""Create BrowseCommand from argparse arguments.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
args: Argparse namespace with command arguments
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
BrowseCommand instance
|
|
53
|
+
"""
|
|
54
|
+
return cls(
|
|
55
|
+
port=args.port,
|
|
56
|
+
query_type=args.query_type,
|
|
57
|
+
query_status=args.query_status,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def execute(self) -> CommandResult:
|
|
61
|
+
"""Execute the browse command.
|
|
62
|
+
|
|
63
|
+
Opens the dashboard in the default browser with optional query parameters.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
CommandResult with success status and URL
|
|
67
|
+
"""
|
|
68
|
+
# Build URL with query params
|
|
69
|
+
url = f"http://localhost:{self.port}"
|
|
70
|
+
|
|
71
|
+
params = []
|
|
72
|
+
if self.query_type:
|
|
73
|
+
params.append(f"type={self.query_type}")
|
|
74
|
+
if self.query_status:
|
|
75
|
+
params.append(f"status={self.query_status}")
|
|
76
|
+
|
|
77
|
+
if params:
|
|
78
|
+
url += "?" + "&".join(params)
|
|
79
|
+
|
|
80
|
+
# Check if server is running
|
|
81
|
+
try:
|
|
82
|
+
import requests # type: ignore[import-untyped]
|
|
83
|
+
|
|
84
|
+
response = requests.head(f"http://localhost:{self.port}", timeout=1)
|
|
85
|
+
response.raise_for_status()
|
|
86
|
+
except ImportError:
|
|
87
|
+
# requests module not available - try to open anyway with a warning
|
|
88
|
+
webbrowser.open(url)
|
|
89
|
+
return CommandResult(
|
|
90
|
+
data={"url": url},
|
|
91
|
+
text=f"Opening dashboard at {url}\n(Note: Could not verify server is running - install 'requests' for server checks)",
|
|
92
|
+
exit_code=0,
|
|
93
|
+
)
|
|
94
|
+
except Exception:
|
|
95
|
+
# Server not running or not responding
|
|
96
|
+
return CommandResult(
|
|
97
|
+
text=f"Dashboard server not running on port {self.port}.\nStart with: htmlgraph serve --port {self.port}",
|
|
98
|
+
exit_code=1,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Open browser
|
|
102
|
+
try:
|
|
103
|
+
webbrowser.open(url)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return CommandResult(
|
|
106
|
+
text=f"Failed to open browser: {e}\nYou can manually visit: {url}",
|
|
107
|
+
exit_code=1,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return CommandResult(
|
|
111
|
+
data={"url": url},
|
|
112
|
+
text=f"Opening dashboard at {url}",
|
|
113
|
+
exit_code=0,
|
|
114
|
+
)
|