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.
- arctx_cli/__init__.py +1 -0
- arctx_cli/alias.py +238 -0
- arctx_cli/append_batch.py +90 -0
- arctx_cli/commands/__init__.py +85 -0
- arctx_cli/commands/alias_cmd.py +174 -0
- arctx_cli/commands/anchor.py +82 -0
- arctx_cli/commands/current.py +69 -0
- arctx_cli/commands/cut.py +89 -0
- arctx_cli/commands/dump.py +72 -0
- arctx_cli/commands/ext.py +236 -0
- arctx_cli/commands/git.py +216 -0
- arctx_cli/commands/graph.py +73 -0
- arctx_cli/commands/guide.py +360 -0
- arctx_cli/commands/init.py +223 -0
- arctx_cli/commands/list.py +45 -0
- arctx_cli/commands/migrate.py +135 -0
- arctx_cli/commands/node.py +55 -0
- arctx_cli/commands/outcomes.py +58 -0
- arctx_cli/commands/payload.py +192 -0
- arctx_cli/commands/reachable.py +75 -0
- arctx_cli/commands/show.py +113 -0
- arctx_cli/commands/sync.py +244 -0
- arctx_cli/commands/trace.py +46 -0
- arctx_cli/commands/transition.py +212 -0
- arctx_cli/commands/use.py +67 -0
- arctx_cli/commands/view.py +82 -0
- arctx_cli/commands/work_session.py +330 -0
- arctx_cli/context.py +38 -0
- arctx_cli/ext/__init__.py +1 -0
- arctx_cli/ext/command/__init__.py +110 -0
- arctx_cli/ext/git/__init__.py +1 -0
- arctx_cli/ext/git/branch.py +140 -0
- arctx_cli/ext/git/cherry_pick.py +144 -0
- arctx_cli/ext/git/commit.py +205 -0
- arctx_cli/ext/git/hook.py +758 -0
- arctx_cli/ext/git/merge.py +204 -0
- arctx_cli/ext/git/reset.py +138 -0
- arctx_cli/ext/git/revert.py +157 -0
- arctx_cli/ext/git/verify.py +140 -0
- arctx_cli/ext/git/worktree.py +173 -0
- arctx_cli/ext_registry.py +34 -0
- arctx_cli/main.py +133 -0
- arctx_cli/paths.py +27 -0
- arctx_cli/payload_builder.py +23 -0
- arctx_cli/workspace.py +64 -0
- arctx_cli-0.2.0b2.dist-info/METADATA +48 -0
- arctx_cli-0.2.0b2.dist-info/RECORD +49 -0
- arctx_cli-0.2.0b2.dist-info/WHEEL +4 -0
- 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
|