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