arctx-cli 0.2.0b2__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.
Files changed (49) hide show
  1. arctx_cli/__init__.py +1 -0
  2. arctx_cli/alias.py +238 -0
  3. arctx_cli/append_batch.py +90 -0
  4. arctx_cli/commands/__init__.py +85 -0
  5. arctx_cli/commands/alias_cmd.py +174 -0
  6. arctx_cli/commands/anchor.py +82 -0
  7. arctx_cli/commands/current.py +69 -0
  8. arctx_cli/commands/cut.py +89 -0
  9. arctx_cli/commands/dump.py +72 -0
  10. arctx_cli/commands/ext.py +236 -0
  11. arctx_cli/commands/git.py +216 -0
  12. arctx_cli/commands/graph.py +73 -0
  13. arctx_cli/commands/guide.py +360 -0
  14. arctx_cli/commands/init.py +223 -0
  15. arctx_cli/commands/list.py +45 -0
  16. arctx_cli/commands/migrate.py +135 -0
  17. arctx_cli/commands/node.py +55 -0
  18. arctx_cli/commands/outcomes.py +58 -0
  19. arctx_cli/commands/payload.py +192 -0
  20. arctx_cli/commands/reachable.py +75 -0
  21. arctx_cli/commands/show.py +113 -0
  22. arctx_cli/commands/sync.py +244 -0
  23. arctx_cli/commands/trace.py +46 -0
  24. arctx_cli/commands/transition.py +212 -0
  25. arctx_cli/commands/use.py +67 -0
  26. arctx_cli/commands/view.py +82 -0
  27. arctx_cli/commands/work_session.py +330 -0
  28. arctx_cli/context.py +38 -0
  29. arctx_cli/ext/__init__.py +1 -0
  30. arctx_cli/ext/command/__init__.py +110 -0
  31. arctx_cli/ext/git/__init__.py +1 -0
  32. arctx_cli/ext/git/branch.py +140 -0
  33. arctx_cli/ext/git/cherry_pick.py +144 -0
  34. arctx_cli/ext/git/commit.py +205 -0
  35. arctx_cli/ext/git/hook.py +758 -0
  36. arctx_cli/ext/git/merge.py +204 -0
  37. arctx_cli/ext/git/reset.py +138 -0
  38. arctx_cli/ext/git/revert.py +157 -0
  39. arctx_cli/ext/git/verify.py +140 -0
  40. arctx_cli/ext/git/worktree.py +173 -0
  41. arctx_cli/ext_registry.py +34 -0
  42. arctx_cli/main.py +133 -0
  43. arctx_cli/paths.py +27 -0
  44. arctx_cli/payload_builder.py +23 -0
  45. arctx_cli/workspace.py +64 -0
  46. arctx_cli-0.2.0b2.dist-info/METADATA +48 -0
  47. arctx_cli-0.2.0b2.dist-info/RECORD +49 -0
  48. arctx_cli-0.2.0b2.dist-info/WHEEL +4 -0
  49. arctx_cli-0.2.0b2.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,135 @@
1
+ """arctx CLI migrate command — convert a jsonl run directory to sqlite."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from arctx.storage.jsonl import JsonlRunStore
11
+
12
+
13
+ def add_parser(subparsers) -> argparse.ArgumentParser:
14
+ """Register the ``migrate`` subcommand parser."""
15
+ parser = subparsers.add_parser(
16
+ "migrate",
17
+ help="Migrate run storage format (currently: jsonl -> sqlite)",
18
+ )
19
+ parser.add_argument(
20
+ "--to",
21
+ required=True,
22
+ choices=["sqlite"],
23
+ help="Target storage format",
24
+ )
25
+ parser.add_argument(
26
+ "--store-dir",
27
+ default=None,
28
+ help="Directory where runs are stored (default: .arctx/runs)",
29
+ )
30
+ target = parser.add_mutually_exclusive_group(required=True)
31
+ target.add_argument("--run", metavar="RUN_ID", help="Single run ID to migrate")
32
+ target.add_argument("--all", action="store_true", help="Migrate all runs in store-dir")
33
+ parser.add_argument(
34
+ "--force",
35
+ action="store_true",
36
+ help="Overwrite existing run.db if present",
37
+ )
38
+ return parser
39
+
40
+
41
+ def run_migrate_command(
42
+ *,
43
+ to: str,
44
+ store_dir: str,
45
+ run_id: str | None,
46
+ all_runs: bool,
47
+ force: bool,
48
+ ) -> dict:
49
+ """Migrate one or all jsonl runs to sqlite.
50
+
51
+ Parameters
52
+ ----------
53
+ to:
54
+ Target format, currently only ``"sqlite"`` is supported.
55
+ store_dir:
56
+ Root directory of the run store.
57
+ run_id:
58
+ Single run to migrate. Mutually exclusive with *all_runs*.
59
+ all_runs:
60
+ When True, migrate all runs found via ``JsonlRunStore.list_runs()``.
61
+ force:
62
+ When True, overwrite an existing ``run.db`` instead of skipping.
63
+
64
+ Returns
65
+ -------
66
+ dict with ``migrated``, ``skipped``, and ``failed`` lists of run IDs.
67
+ """
68
+ if to != "sqlite":
69
+ raise ValueError(f"unsupported target format: {to!r}")
70
+
71
+ src_store = JsonlRunStore(store_dir)
72
+ from arctx.storage.sqlite import SqliteRunStore
73
+
74
+ dst_store = SqliteRunStore(store_dir)
75
+
76
+ if all_runs:
77
+ run_ids = [r["run_id"] for r in src_store.list_runs()]
78
+ else:
79
+ if run_id is None:
80
+ raise ValueError("either --run or --all must be specified")
81
+ run_ids = [run_id]
82
+
83
+ migrated: list[str] = []
84
+ skipped: list[str] = []
85
+ failed: list[str] = []
86
+
87
+ for rid in run_ids:
88
+ run_path = Path(store_dir) / rid
89
+ nodes_jsonl = run_path / "nodes.jsonl"
90
+ db_path = run_path / "run.db"
91
+
92
+ # Must look like a jsonl run dir
93
+ if not nodes_jsonl.exists():
94
+ print(
95
+ f"warning: {rid}: nodes.jsonl not found — not a jsonl run, skipping",
96
+ file=sys.stderr,
97
+ )
98
+ skipped.append(rid)
99
+ continue
100
+
101
+ # Already migrated?
102
+ if db_path.exists() and not force:
103
+ print(
104
+ f"warning: {rid}: run.db already exists — skipping (use --force to overwrite)",
105
+ file=sys.stderr,
106
+ )
107
+ skipped.append(rid)
108
+ continue
109
+
110
+ # Remove stale db so SqliteRunStore starts fresh
111
+ if db_path.exists() and force:
112
+ db_path.unlink()
113
+
114
+ try:
115
+ handle = src_store.load_run(rid)
116
+ dst_store.save_run(handle)
117
+ migrated.append(rid)
118
+ except Exception as exc: # noqa: BLE001
119
+ print(f"error: {rid}: migration failed — {exc}", file=sys.stderr)
120
+ failed.append(rid)
121
+
122
+ return {"migrated": migrated, "skipped": skipped, "failed": failed}
123
+
124
+
125
+ def cli_migrate(args) -> int:
126
+ """Entry point for ``arctx migrate`` subcommand."""
127
+ result = run_migrate_command(
128
+ to=args.to,
129
+ store_dir=args.store_dir,
130
+ run_id=getattr(args, "run", None),
131
+ all_runs=args.all,
132
+ force=args.force,
133
+ )
134
+ print(json.dumps(result, ensure_ascii=False, indent=2))
135
+ return 0 if not result["failed"] else 1
@@ -0,0 +1,55 @@
1
+ """arctx node commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+
8
+ from arctx_cli.commands.show import run_show_command
9
+ from arctx_cli.context import resolve_run_id_from_args
10
+
11
+
12
+ def add_parser(subparsers) -> argparse.ArgumentParser:
13
+ parser = subparsers.add_parser("node", help="Inspect nodes")
14
+ node_sub = parser.add_subparsers(dest="node_command", required=True)
15
+
16
+ sp_show = node_sub.add_parser("show", help="Show one node")
17
+ sp_show.add_argument("node_id")
18
+ sp_show.add_argument("--with-payloads", action="store_true")
19
+ sp_show.add_argument("--run", default=None)
20
+ sp_show.add_argument("--store-dir", default=None)
21
+
22
+ sp_payloads = node_sub.add_parser("payloads", help="Show node payloads")
23
+ sp_payloads.add_argument("node_id")
24
+ sp_payloads.add_argument("--run", default=None)
25
+ sp_payloads.add_argument("--store-dir", default=None)
26
+
27
+ return parser
28
+
29
+
30
+ def cli_node(args) -> int:
31
+ if args.node_command == "show":
32
+ result = run_show_command(
33
+ run_id=resolve_run_id_from_args(args),
34
+ node_id=args.node_id,
35
+ transition_id=None,
36
+ payload_id=None,
37
+ with_payloads=args.with_payloads,
38
+ outputs=False,
39
+ store_dir=args.store_dir,
40
+ )
41
+ print(json.dumps(result, ensure_ascii=False, indent=2))
42
+ return 0
43
+ if args.node_command == "payloads":
44
+ result = run_show_command(
45
+ run_id=resolve_run_id_from_args(args),
46
+ node_id=args.node_id,
47
+ transition_id=None,
48
+ payload_id=None,
49
+ with_payloads=True,
50
+ outputs=False,
51
+ store_dir=args.store_dir,
52
+ )
53
+ print(json.dumps(result["payloads"], ensure_ascii=False, indent=2))
54
+ return 0
55
+ return 1
@@ -0,0 +1,58 @@
1
+ """arctx CLI outcomes command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+
8
+ from arctx_cli.context import resolve_store, resolve_run_id_from_args
9
+
10
+
11
+ def add_parser(subparsers) -> argparse.ArgumentParser:
12
+ parser = subparsers.add_parser("outcomes", help="List output nodes for a Transition")
13
+ parser.add_argument("transition_id", help="Transition to inspect")
14
+ parser.add_argument("--run", default=None)
15
+ parser.add_argument("--include-payloads", action="store_true")
16
+ parser.add_argument("--store-dir", default=None)
17
+ return parser
18
+
19
+
20
+ def run_outcomes_command(
21
+ *,
22
+ run_id: str,
23
+ transition_id: str,
24
+ include_payloads: bool,
25
+ store_dir: str,
26
+ ) -> dict:
27
+ store = resolve_store(store_dir)
28
+ if not store.run_path(run_id).exists():
29
+ raise KeyError(f"unknown run_id: {run_id}")
30
+ handle = store.load_run(run_id)
31
+ g = handle.run_graph
32
+
33
+ result = handle.outcomes(transition_id)
34
+
35
+ if include_payloads:
36
+ result["transition_payloads"] = [
37
+ p.to_dict() for p in g.payloads_for_transition(transition_id)
38
+ ]
39
+ result["output_nodes"] = [
40
+ {
41
+ "node": g.nodes[node_id].to_dict(),
42
+ "payloads": [p.to_dict() for p in g.payloads_for_node(node_id)],
43
+ }
44
+ for node_id in result["output_node_ids"]
45
+ ]
46
+
47
+ return result
48
+
49
+
50
+ def cli_outcomes(args) -> int:
51
+ result = run_outcomes_command(
52
+ run_id=resolve_run_id_from_args(args),
53
+ transition_id=args.transition_id,
54
+ include_payloads=args.include_payloads,
55
+ store_dir=args.store_dir,
56
+ )
57
+ print(json.dumps(result, ensure_ascii=False, indent=2))
58
+ return 0
@@ -0,0 +1,192 @@
1
+ """arctx payload commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+
8
+ from arctx_cli.append_batch import graph_counts, maybe_append_or_save
9
+ from arctx_cli.context import (
10
+ resolve_run_id_from_args,
11
+ resolve_store,
12
+ resolve_user_id_from_args,
13
+ resolve_work_session_id_from_args,
14
+ )
15
+ from arctx_cli.payload_builder import (
16
+ build_payload,
17
+ parse_field_args,
18
+ parse_json_object,
19
+ payload_schema,
20
+ payload_type_names,
21
+ )
22
+
23
+
24
+ def add_parser(subparsers) -> argparse.ArgumentParser:
25
+ parser = subparsers.add_parser("payload", help="Inspect and attach payloads")
26
+ payload_sub = parser.add_subparsers(dest="payload_command", required=True)
27
+
28
+ sp_types = payload_sub.add_parser("types", help="List registered payload types")
29
+ sp_types.add_argument("--store-dir", default=None)
30
+
31
+ sp_schema = payload_sub.add_parser("schema", help="Show payload type fields")
32
+ sp_schema.add_argument("payload_type")
33
+ sp_schema.add_argument("--store-dir", default=None)
34
+
35
+ sp_add = payload_sub.add_parser("add", help="Attach a payload to a node or transition")
36
+ target = sp_add.add_mutually_exclusive_group(required=True)
37
+ target.add_argument("--node", dest="node_id", default=None)
38
+ target.add_argument("--transition", dest="transition_id", default=None)
39
+ sp_add.add_argument("--payload-type", required=True)
40
+ sp_add.add_argument("--field", action="append", default=None, help="Payload field as key=value")
41
+ sp_add.add_argument("--json", default=None, help="Payload fields as a JSON object")
42
+ sp_add.add_argument("--run", default=None)
43
+ sp_add.add_argument("--store-dir", default=None)
44
+ sp_add.add_argument("--user", default=None)
45
+ sp_add.add_argument("--work-session", default=None)
46
+
47
+ sp_list = payload_sub.add_parser("list", help="List payloads on a node or transition")
48
+ target = sp_list.add_mutually_exclusive_group(required=True)
49
+ target.add_argument("--node", dest="node_id", default=None)
50
+ target.add_argument("--transition", dest="transition_id", default=None)
51
+ sp_list.add_argument("--run", default=None)
52
+ sp_list.add_argument("--store-dir", default=None)
53
+
54
+ sp_show = payload_sub.add_parser("show", help="Show one payload")
55
+ sp_show.add_argument("payload_id")
56
+ sp_show.add_argument("--run", default=None)
57
+ sp_show.add_argument("--store-dir", default=None)
58
+
59
+ return parser
60
+
61
+
62
+ def run_payload_add_command(
63
+ *,
64
+ run_id: str,
65
+ target_kind: str,
66
+ target_id: str,
67
+ payload_type: str,
68
+ field_data: dict | None,
69
+ json_data: dict | None,
70
+ store_dir: str,
71
+ user_id: str | None = None,
72
+ work_session_id: str | None = None,
73
+ ) -> dict:
74
+ store = resolve_store(store_dir)
75
+ if not store.run_path(run_id).exists():
76
+ raise KeyError(f"unknown run_id: {run_id}")
77
+ handle = store.load_run(run_id)
78
+ before = graph_counts(handle)
79
+ payload = build_payload(
80
+ payload_type=payload_type,
81
+ target_kind=target_kind, # type: ignore[arg-type]
82
+ target_id=target_id,
83
+ payload_id=handle._next_id("pl"),
84
+ json_data=json_data,
85
+ field_data=field_data,
86
+ )
87
+ if payload.target_kind == "node":
88
+ attached = handle.attach(
89
+ payload.target_id,
90
+ payload,
91
+ user_id=user_id,
92
+ work_session_id=work_session_id,
93
+ )
94
+ else:
95
+ handle.run_graph.attach_payload(payload)
96
+ handle.record_work_event(
97
+ user_id=user_id,
98
+ work_session_id=work_session_id,
99
+ event_type="payload_attached",
100
+ target_kind="transition",
101
+ target_id=payload.target_id,
102
+ created_records=(payload.payload_id,),
103
+ summary=payload.payload_type,
104
+ )
105
+ attached = payload
106
+ maybe_append_or_save(
107
+ store=store,
108
+ handle=handle,
109
+ user_id=user_id,
110
+ work_session_id=work_session_id,
111
+ before=before,
112
+ )
113
+ return {"payload": attached.to_dict()}
114
+
115
+
116
+ def run_payload_list_command(
117
+ *,
118
+ run_id: str,
119
+ target_kind: str,
120
+ target_id: str,
121
+ store_dir: str,
122
+ ) -> dict:
123
+ store = resolve_store(store_dir)
124
+ if not store.run_path(run_id).exists():
125
+ raise KeyError(f"unknown run_id: {run_id}")
126
+ handle = store.load_run(run_id)
127
+ g = handle.run_graph
128
+ if target_kind == "node":
129
+ if target_id not in g.nodes:
130
+ raise KeyError(f"unknown node_id: {target_id}")
131
+ payloads = g.payloads_for_node(target_id)
132
+ else:
133
+ if target_id not in g.transitions:
134
+ raise KeyError(f"unknown transition_id: {target_id}")
135
+ payloads = g.payloads_for_transition(target_id)
136
+ return {"payloads": [p.to_dict() for p in payloads]}
137
+
138
+
139
+ def run_payload_show_command(*, run_id: str, payload_id: str, store_dir: str) -> dict:
140
+ store = resolve_store(store_dir)
141
+ if not store.run_path(run_id).exists():
142
+ raise KeyError(f"unknown run_id: {run_id}")
143
+ handle = store.load_run(run_id)
144
+ payload = handle.run_graph.payloads.get(payload_id)
145
+ if payload is None:
146
+ raise KeyError(f"unknown payload_id: {payload_id}")
147
+ return {"payload": payload.to_dict()}
148
+
149
+
150
+ def cli_payload(args) -> int:
151
+ if args.payload_command == "types":
152
+ print(json.dumps({"payload_types": payload_type_names()}, ensure_ascii=False, indent=2))
153
+ return 0
154
+ if args.payload_command == "schema":
155
+ print(json.dumps(payload_schema(args.payload_type), ensure_ascii=False, indent=2))
156
+ return 0
157
+ if args.payload_command == "add":
158
+ target_kind = "node" if args.node_id is not None else "transition"
159
+ target_id = args.node_id if args.node_id is not None else args.transition_id
160
+ result = run_payload_add_command(
161
+ run_id=resolve_run_id_from_args(args),
162
+ target_kind=target_kind,
163
+ target_id=target_id,
164
+ payload_type=args.payload_type,
165
+ field_data=parse_field_args(args.field),
166
+ json_data=parse_json_object(args.json),
167
+ store_dir=args.store_dir,
168
+ user_id=resolve_user_id_from_args(args),
169
+ work_session_id=resolve_work_session_id_from_args(args),
170
+ )
171
+ print(json.dumps(result["payload"], ensure_ascii=False, indent=2))
172
+ return 0
173
+ if args.payload_command == "list":
174
+ target_kind = "node" if args.node_id is not None else "transition"
175
+ target_id = args.node_id if args.node_id is not None else args.transition_id
176
+ result = run_payload_list_command(
177
+ run_id=resolve_run_id_from_args(args),
178
+ target_kind=target_kind,
179
+ target_id=target_id,
180
+ store_dir=args.store_dir,
181
+ )
182
+ print(json.dumps(result["payloads"], ensure_ascii=False, indent=2))
183
+ return 0
184
+ if args.payload_command == "show":
185
+ result = run_payload_show_command(
186
+ run_id=resolve_run_id_from_args(args),
187
+ payload_id=args.payload_id,
188
+ store_dir=args.store_dir,
189
+ )
190
+ print(json.dumps(result["payload"], ensure_ascii=False, indent=2))
191
+ return 0
192
+ return 1
@@ -0,0 +1,75 @@
1
+ """arctx CLI reachable command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+
8
+ from arctx_cli.context import resolve_store, resolve_run_id_from_args
9
+
10
+
11
+ def add_parser(subparsers) -> argparse.ArgumentParser:
12
+ parser = subparsers.add_parser(
13
+ "reachable", help="Show active subgraph forward-reachable from a node or view"
14
+ )
15
+ parser.add_argument("--run", default=None)
16
+ group = parser.add_mutually_exclusive_group(required=True)
17
+ group.add_argument("--from-node", dest="from_node", default=None)
18
+ group.add_argument("--view", dest="view_name", default=None)
19
+ parser.add_argument("--include-records", action="store_true")
20
+ parser.add_argument("--store-dir", default=None)
21
+ return parser
22
+
23
+
24
+ def run_reachable_command(
25
+ *,
26
+ run_id: str,
27
+ from_node: str | None,
28
+ view_name: str | None,
29
+ include_records: bool,
30
+ store_dir: str,
31
+ ) -> dict:
32
+ if from_node is None and view_name is None:
33
+ raise ValueError("either from_node or view_name is required")
34
+ if from_node is not None and view_name is not None:
35
+ raise ValueError("from_node and view_name are mutually exclusive")
36
+
37
+ store = resolve_store(store_dir)
38
+ if not store.run_path(run_id).exists():
39
+ raise KeyError(f"unknown run_id: {run_id}")
40
+ handle = store.load_run(run_id)
41
+ g = handle.run_graph
42
+
43
+ if view_name is not None:
44
+ view = handle.view_show(view_name)
45
+ root_node_id = view.root_node_id
46
+ else:
47
+ root_node_id = from_node
48
+
49
+ if root_node_id not in g.nodes:
50
+ raise KeyError(f"unknown node_id: {root_node_id}")
51
+
52
+ result: dict = {"root_node_id": root_node_id}
53
+ reachable = g.reachable_from(root_node_id)
54
+ result.update(reachable)
55
+
56
+ if include_records:
57
+ result["nodes"] = [g.nodes[nid].to_dict() for nid in reachable["node_ids"]]
58
+ result["transitions"] = [
59
+ g.transitions[tid].to_dict() for tid in reachable["transition_ids"]
60
+ ]
61
+ result["payloads"] = [g.payloads[pl_id].to_dict() for pl_id in reachable["payload_ids"]]
62
+
63
+ return result
64
+
65
+
66
+ def cli_reachable(args) -> int:
67
+ result = run_reachable_command(
68
+ run_id=resolve_run_id_from_args(args),
69
+ from_node=args.from_node,
70
+ view_name=args.view_name,
71
+ include_records=args.include_records,
72
+ store_dir=args.store_dir,
73
+ )
74
+ print(json.dumps(result, ensure_ascii=False, indent=2))
75
+ return 0
@@ -0,0 +1,113 @@
1
+ """arctx CLI show command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+
8
+ from arctx_cli.context import resolve_store, resolve_run_id_from_args
9
+
10
+
11
+ def add_parser(subparsers) -> argparse.ArgumentParser:
12
+ parser = subparsers.add_parser("show", help="Show run details")
13
+ parser.add_argument("--run", default=None)
14
+ parser.add_argument("--node", dest="node_id", default=None)
15
+ parser.add_argument("--transition", dest="transition_id", default=None)
16
+ parser.add_argument("--payload", dest="payload_id", default=None)
17
+ parser.add_argument("--with-payloads", action="store_true")
18
+ parser.add_argument(
19
+ "--outputs", action="store_true", help="(with --transition) include output nodes"
20
+ )
21
+ parser.add_argument(
22
+ "--history",
23
+ action="store_true",
24
+ help="(with --transition) show all GitChangePayload entries in append order",
25
+ )
26
+ parser.add_argument("--store-dir", default=None)
27
+ return parser
28
+
29
+
30
+ def run_show_command(
31
+ *,
32
+ run_id: str,
33
+ node_id: str | None,
34
+ transition_id: str | None,
35
+ payload_id: str | None,
36
+ with_payloads: bool,
37
+ outputs: bool,
38
+ history: bool = False,
39
+ store_dir: str,
40
+ ) -> dict:
41
+ if outputs and transition_id is None:
42
+ raise ValueError("--outputs can only be used with --transition")
43
+ if with_payloads and payload_id is not None:
44
+ raise ValueError("--with-payloads cannot be used with --payload")
45
+
46
+ store = resolve_store(store_dir)
47
+ if not store.run_path(run_id).exists():
48
+ raise KeyError(f"unknown run_id: {run_id}")
49
+ handle = store.load_run(run_id)
50
+ g = handle.run_graph
51
+
52
+ if node_id is not None:
53
+ node = g.nodes.get(node_id)
54
+ if node is None:
55
+ raise KeyError(f"unknown node_id: {node_id}")
56
+ result: dict = {"node": node.to_dict()}
57
+ if with_payloads:
58
+ result["payloads"] = [p.to_dict() for p in g.payloads_for_node(node_id)]
59
+ return result
60
+
61
+ if transition_id is not None:
62
+ transition = g.transitions.get(transition_id)
63
+ if transition is None:
64
+ raise KeyError(f"unknown transition_id: {transition_id}")
65
+ result = {
66
+ "transition": transition.to_dict(),
67
+ "input_node_ids": g.transition_inputs(transition_id),
68
+ }
69
+ if with_payloads:
70
+ result["payloads"] = [p.to_dict() for p in g.payloads_for_transition(transition_id)]
71
+ if outputs:
72
+ result["output_nodes"] = [
73
+ g.nodes[node_id].to_dict() for node_id in g.transition_outputs(transition_id)
74
+ ]
75
+ # GitChangePayload display: --history shows all; default shows latest only.
76
+ git_payloads = g.payloads_for_transition(transition_id, payload_type="git_change")
77
+ if history:
78
+ result["git_change_history"] = [p.to_dict() for p in git_payloads]
79
+ else:
80
+ result["git_change"] = git_payloads[-1].to_dict() if git_payloads else None
81
+ return result
82
+
83
+ if payload_id is not None:
84
+ payload = g.payloads.get(payload_id)
85
+ if payload is None:
86
+ raise KeyError(f"unknown payload_id: {payload_id}")
87
+ return {"payload": payload.to_dict()}
88
+
89
+ return {
90
+ "run_id": handle.run_id,
91
+ "requirement_id": handle.requirement.requirement_id,
92
+ "root_node_id": handle.root_node_id,
93
+ "node_count": len(g.nodes),
94
+ "transition_count": len(g.transitions),
95
+
96
+ "payload_count": len(g.payloads),
97
+ "views": [v.name for v in handle.run_graph.views.values()],
98
+ }
99
+
100
+
101
+ def cli_show(args) -> int:
102
+ result = run_show_command(
103
+ run_id=resolve_run_id_from_args(args),
104
+ node_id=args.node_id,
105
+ transition_id=args.transition_id,
106
+ payload_id=args.payload_id,
107
+ with_payloads=args.with_payloads,
108
+ outputs=args.outputs,
109
+ history=args.history,
110
+ store_dir=args.store_dir,
111
+ )
112
+ print(json.dumps(result, ensure_ascii=False, indent=2))
113
+ return 0