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,69 @@
|
|
|
1
|
+
"""arctx CLI current command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from arctx_cli.paths import find_repo_root, read_arctx_id, resolve_arctx_home
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
13
|
+
"""Register the ``current`` subcommand parser."""
|
|
14
|
+
parser = subparsers.add_parser("current", help="Show the current run (from <gitdir>/arctx-id)")
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--store-dir",
|
|
17
|
+
default=None,
|
|
18
|
+
help="Directory where runs are stored (default: <ARCTX_HOME>/runs)",
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--json",
|
|
22
|
+
action="store_true",
|
|
23
|
+
dest="json_output",
|
|
24
|
+
help="Output as JSON",
|
|
25
|
+
)
|
|
26
|
+
return parser
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run_current_command(
|
|
30
|
+
*,
|
|
31
|
+
store_dir: str | None = None,
|
|
32
|
+
) -> dict:
|
|
33
|
+
"""Show the current run resolved from ``<gitdir>/arctx-id``.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
store_dir:
|
|
38
|
+
Ignored (kept for API compatibility). The run path is derived from
|
|
39
|
+
ARCTX_HOME and the run_id in ``<gitdir>/arctx-id``.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
dict with ``run_id`` and ``run_path`` keys.
|
|
44
|
+
|
|
45
|
+
Raises
|
|
46
|
+
------
|
|
47
|
+
RuntimeError
|
|
48
|
+
If not in a git repo or no ``<gitdir>/arctx-id`` is present.
|
|
49
|
+
"""
|
|
50
|
+
repo_root = find_repo_root()
|
|
51
|
+
run_id = read_arctx_id(repo_root)
|
|
52
|
+
if not run_id:
|
|
53
|
+
raise RuntimeError(
|
|
54
|
+
"no current run set. "
|
|
55
|
+
"Run 'arctx init' to create a run or 'arctx use <run_id>' to set one."
|
|
56
|
+
)
|
|
57
|
+
arctx_home = resolve_arctx_home()
|
|
58
|
+
run_path = str(arctx_home/ "runs" / run_id)
|
|
59
|
+
return {"run_id": run_id, "run_path": run_path}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def cli_current(args) -> int:
|
|
63
|
+
"""Entry point for ``arctx current`` subcommand."""
|
|
64
|
+
result = run_current_command(store_dir=getattr(args, "store_dir", None))
|
|
65
|
+
if args.json_output:
|
|
66
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
67
|
+
else:
|
|
68
|
+
print(result["run_id"])
|
|
69
|
+
return 0
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""arctx CLI cut command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from arctx_cli.context import (
|
|
9
|
+
resolve_run_id_from_args,
|
|
10
|
+
resolve_store,
|
|
11
|
+
resolve_user_id_from_args,
|
|
12
|
+
resolve_work_session_id_from_args,
|
|
13
|
+
)
|
|
14
|
+
from arctx_cli.append_batch import graph_counts, maybe_append_or_save
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
18
|
+
parser = subparsers.add_parser("cut", help="Cut a Node or Transition")
|
|
19
|
+
parser.add_argument("kind", nargs="?", choices=["node", "transition"])
|
|
20
|
+
parser.add_argument("id", nargs="?")
|
|
21
|
+
group = parser.add_mutually_exclusive_group(required=False)
|
|
22
|
+
group.add_argument("--node", dest="node_id", metavar="NODE_ID")
|
|
23
|
+
group.add_argument("--transition", dest="transition_id", metavar="TRANSITION_ID")
|
|
24
|
+
parser.add_argument("--run", default=None)
|
|
25
|
+
parser.add_argument("--reason", default=None)
|
|
26
|
+
parser.add_argument("--store-dir", default=None)
|
|
27
|
+
parser.add_argument("--user", default=None)
|
|
28
|
+
parser.add_argument("--work-session", default=None)
|
|
29
|
+
return parser
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_cut_command(
|
|
33
|
+
*,
|
|
34
|
+
run_id: str,
|
|
35
|
+
target_id: str,
|
|
36
|
+
target_kind: str,
|
|
37
|
+
reason: str | None,
|
|
38
|
+
store_dir: str,
|
|
39
|
+
user_id: str | None = None,
|
|
40
|
+
work_session_id: str | None = None,
|
|
41
|
+
) -> dict:
|
|
42
|
+
store = resolve_store(store_dir)
|
|
43
|
+
if not store.run_path(run_id).exists():
|
|
44
|
+
raise KeyError(f"unknown run_id: {run_id}")
|
|
45
|
+
handle = store.load_run(run_id)
|
|
46
|
+
before = graph_counts(handle)
|
|
47
|
+
cut = handle.cut(
|
|
48
|
+
target_id,
|
|
49
|
+
target_kind=target_kind, # type: ignore[arg-type]
|
|
50
|
+
reason=reason,
|
|
51
|
+
user_id=user_id,
|
|
52
|
+
work_session_id=work_session_id,
|
|
53
|
+
)
|
|
54
|
+
maybe_append_or_save(
|
|
55
|
+
store=store,
|
|
56
|
+
handle=handle,
|
|
57
|
+
user_id=user_id,
|
|
58
|
+
work_session_id=work_session_id,
|
|
59
|
+
before=before,
|
|
60
|
+
)
|
|
61
|
+
return {"cut": cut.to_dict()}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def cli_cut(args) -> int:
|
|
65
|
+
if args.kind is not None:
|
|
66
|
+
if args.id is None:
|
|
67
|
+
raise ValueError("cut requires an id when using positional target")
|
|
68
|
+
target_id = args.id
|
|
69
|
+
target_kind = args.kind
|
|
70
|
+
elif args.node_id is not None:
|
|
71
|
+
target_id = args.node_id
|
|
72
|
+
target_kind = "node"
|
|
73
|
+
elif args.transition_id is not None:
|
|
74
|
+
target_id = args.transition_id
|
|
75
|
+
target_kind = "transition"
|
|
76
|
+
else:
|
|
77
|
+
raise ValueError("provide 'node <id>', 'transition <id>', --node, or --transition")
|
|
78
|
+
|
|
79
|
+
result = run_cut_command(
|
|
80
|
+
run_id=resolve_run_id_from_args(args),
|
|
81
|
+
target_id=target_id,
|
|
82
|
+
target_kind=target_kind,
|
|
83
|
+
reason=args.reason,
|
|
84
|
+
store_dir=args.store_dir,
|
|
85
|
+
user_id=resolve_user_id_from_args(args),
|
|
86
|
+
work_session_id=resolve_work_session_id_from_args(args),
|
|
87
|
+
)
|
|
88
|
+
print(json.dumps(result["cut"], ensure_ascii=False, indent=2))
|
|
89
|
+
return 0
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""arctx CLI dump command: render a run as outline or mermaid."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from arctx_cli.context import resolve_store, resolve_run_id_from_args
|
|
8
|
+
from arctx.core.run.dump import DumpOptions, dump
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
12
|
+
parser = subparsers.add_parser(
|
|
13
|
+
"dump",
|
|
14
|
+
help="Render the run as an outline (LLM-friendly) or mermaid (visual)",
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--format",
|
|
18
|
+
dest="fmt",
|
|
19
|
+
choices=["outline", "mermaid"],
|
|
20
|
+
default="outline",
|
|
21
|
+
help="Output format (default: outline)",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument("--node", dest="node_id", default=None,
|
|
24
|
+
help="Render only the subtree rooted at this node")
|
|
25
|
+
parser.add_argument("--depth", type=int, default=None,
|
|
26
|
+
help="Limit traversal depth")
|
|
27
|
+
parser.add_argument("--observed-only", action="store_true",
|
|
28
|
+
help="Hide predicted output transitions")
|
|
29
|
+
parser.add_argument("--predicted-only", action="store_true",
|
|
30
|
+
help="Hide observed (result) output transitions")
|
|
31
|
+
parser.add_argument("--full-payloads", action="store_true",
|
|
32
|
+
help="Include full payload metrics / rationale")
|
|
33
|
+
parser.add_argument("--run", default=None)
|
|
34
|
+
parser.add_argument("--store-dir", default=None)
|
|
35
|
+
return parser
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_dump_command(
|
|
39
|
+
*,
|
|
40
|
+
run_id: str,
|
|
41
|
+
fmt: str = "outline",
|
|
42
|
+
store_dir: str = ".arctx/runs",
|
|
43
|
+
node_id: str | None = None,
|
|
44
|
+
depth: int | None = None,
|
|
45
|
+
full_payloads: bool = False,
|
|
46
|
+
) -> str:
|
|
47
|
+
store = resolve_store(store_dir)
|
|
48
|
+
if not store.run_path(run_id).exists():
|
|
49
|
+
raise KeyError(f"unknown run_id: {run_id}")
|
|
50
|
+
handle = store.load_run(run_id)
|
|
51
|
+
opts = DumpOptions(node_id=node_id, depth=depth, full_payloads=full_payloads)
|
|
52
|
+
return dump(handle, fmt, opts)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def cli_dump(args) -> int:
|
|
56
|
+
if args.observed_only and args.predicted_only:
|
|
57
|
+
raise ValueError("--observed-only and --predicted-only are mutually exclusive")
|
|
58
|
+
store = resolve_store(args.store_dir)
|
|
59
|
+
run_id = resolve_run_id_from_args(args)
|
|
60
|
+
if not store.run_path(run_id).exists():
|
|
61
|
+
raise KeyError(f"unknown run_id: {run_id}")
|
|
62
|
+
handle = store.load_run(run_id)
|
|
63
|
+
|
|
64
|
+
opts = DumpOptions(
|
|
65
|
+
node_id=args.node_id,
|
|
66
|
+
depth=args.depth,
|
|
67
|
+
observed_only=args.observed_only,
|
|
68
|
+
predicted_only=args.predicted_only,
|
|
69
|
+
full_payloads=args.full_payloads,
|
|
70
|
+
)
|
|
71
|
+
print(dump(handle, args.fmt, opts))
|
|
72
|
+
return 0
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""arctx ext subcommand — manage extensions for a run.
|
|
2
|
+
|
|
3
|
+
Subcommands
|
|
4
|
+
-----------
|
|
5
|
+
list List all built-in extensions and whether each is enabled in the current run.
|
|
6
|
+
show Show details (version, default_aliases) for a named extension as JSON.
|
|
7
|
+
enable Enable an extension in an existing run (calls on_init, then records it).
|
|
8
|
+
disable Remove an extension from the enabled list (side effects are NOT reversed).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
19
|
+
"""Register the ``ext`` subcommand."""
|
|
20
|
+
parser = subparsers.add_parser("ext", help="Manage extensions")
|
|
21
|
+
ext_sub = parser.add_subparsers(dest="ext_command", required=True)
|
|
22
|
+
|
|
23
|
+
# ext list
|
|
24
|
+
_list = ext_sub.add_parser("list", help="List available extensions")
|
|
25
|
+
_list.add_argument("--run", default=None, help="Run ID")
|
|
26
|
+
_list.add_argument("--store-dir", default=None, dest="store_dir")
|
|
27
|
+
|
|
28
|
+
# ext show <name>
|
|
29
|
+
show = ext_sub.add_parser("show", help="Show extension details")
|
|
30
|
+
show.add_argument("name", help="Extension name")
|
|
31
|
+
|
|
32
|
+
# ext enable <name>
|
|
33
|
+
enable = ext_sub.add_parser("enable", help="Enable an extension in the current run")
|
|
34
|
+
enable.add_argument("name", help="Extension name")
|
|
35
|
+
enable.add_argument("--run", default=None, help="Run ID")
|
|
36
|
+
enable.add_argument("--store-dir", default=None, dest="store_dir")
|
|
37
|
+
|
|
38
|
+
# ext disable <name>
|
|
39
|
+
disable = ext_sub.add_parser("disable", help="Disable an extension in the current run")
|
|
40
|
+
disable.add_argument("name", help="Extension name")
|
|
41
|
+
disable.add_argument("--run", default=None, help="Run ID")
|
|
42
|
+
disable.add_argument("--store-dir", default=None, dest="store_dir")
|
|
43
|
+
|
|
44
|
+
return parser
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Command runners
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_ext_list_command(
|
|
53
|
+
*,
|
|
54
|
+
run_dir: str | None = None,
|
|
55
|
+
) -> dict:
|
|
56
|
+
"""Return info about all built-in extensions.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
run_dir:
|
|
61
|
+
When provided, each extension's enabled status is checked against
|
|
62
|
+
the run's ``extensions.json``.
|
|
63
|
+
"""
|
|
64
|
+
from arctx.ext import list_available
|
|
65
|
+
from arctx.ext.enabled import load_enabled
|
|
66
|
+
|
|
67
|
+
available = list_available()
|
|
68
|
+
enabled_names: set[str] = set()
|
|
69
|
+
if run_dir is not None:
|
|
70
|
+
enabled_names = {ee.name for ee in load_enabled(run_dir)}
|
|
71
|
+
|
|
72
|
+
items = []
|
|
73
|
+
for name in available:
|
|
74
|
+
items.append({"name": name, "enabled": name in enabled_names})
|
|
75
|
+
return {"extensions": items}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def run_ext_show_command(name: str) -> dict:
|
|
79
|
+
"""Return details for a named extension.
|
|
80
|
+
|
|
81
|
+
Raises
|
|
82
|
+
------
|
|
83
|
+
KeyError
|
|
84
|
+
If *name* is not registered.
|
|
85
|
+
"""
|
|
86
|
+
from arctx.ext import load_extension
|
|
87
|
+
|
|
88
|
+
ext = load_extension(name)
|
|
89
|
+
return {
|
|
90
|
+
"name": ext.name,
|
|
91
|
+
"version": ext.version,
|
|
92
|
+
"default_aliases": ext.default_aliases(),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def run_ext_enable_command(
|
|
97
|
+
*,
|
|
98
|
+
name: str,
|
|
99
|
+
run_dir: str,
|
|
100
|
+
) -> dict:
|
|
101
|
+
"""Enable extension *name* in *run_dir*.
|
|
102
|
+
|
|
103
|
+
Calls ``on_init`` with an empty options dict (no parser options available
|
|
104
|
+
after init), then records the extension as enabled.
|
|
105
|
+
|
|
106
|
+
Raises
|
|
107
|
+
------
|
|
108
|
+
KeyError
|
|
109
|
+
If *name* is not in the registry.
|
|
110
|
+
"""
|
|
111
|
+
from arctx.ext import load_extension
|
|
112
|
+
from arctx.ext.base import InitContext
|
|
113
|
+
from arctx.ext.enabled import EnabledExtension, add_enabled, load_enabled
|
|
114
|
+
from arctx.storage.jsonl import JsonlRunStore
|
|
115
|
+
|
|
116
|
+
# Resolve run_id from the run dir (read run.json)
|
|
117
|
+
import json as _json
|
|
118
|
+
from pathlib import Path as _Path
|
|
119
|
+
|
|
120
|
+
run_json = _Path(run_dir) / "run.json"
|
|
121
|
+
run_id = _json.loads(run_json.read_text(encoding="utf-8"))["run_id"]
|
|
122
|
+
|
|
123
|
+
ext = load_extension(name)
|
|
124
|
+
|
|
125
|
+
# Check if already enabled
|
|
126
|
+
current = load_enabled(run_dir)
|
|
127
|
+
if any(e.name == name for e in current):
|
|
128
|
+
return {"status": "already_enabled", "name": name}
|
|
129
|
+
|
|
130
|
+
ctx = InitContext(run_id=run_id, run_dir=run_dir, options={})
|
|
131
|
+
ext.on_init(ctx)
|
|
132
|
+
|
|
133
|
+
add_enabled(run_dir, EnabledExtension(name=ext.name, version=ext.version, config={}))
|
|
134
|
+
return {"status": "enabled", "name": name}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def run_ext_disable_command(
|
|
138
|
+
*,
|
|
139
|
+
name: str,
|
|
140
|
+
run_dir: str,
|
|
141
|
+
) -> dict:
|
|
142
|
+
"""Remove extension *name* from the enabled list.
|
|
143
|
+
|
|
144
|
+
Does NOT reverse any side effects (files written, hooks installed, etc.).
|
|
145
|
+
Callers should warn the user about this.
|
|
146
|
+
|
|
147
|
+
Raises
|
|
148
|
+
------
|
|
149
|
+
KeyError
|
|
150
|
+
If *name* is not currently enabled.
|
|
151
|
+
"""
|
|
152
|
+
from arctx.ext.enabled import load_enabled, save_enabled
|
|
153
|
+
|
|
154
|
+
current = load_enabled(run_dir)
|
|
155
|
+
remaining = [e for e in current if e.name != name]
|
|
156
|
+
if len(remaining) == len(current):
|
|
157
|
+
raise KeyError(f"extension {name!r} is not enabled in this run")
|
|
158
|
+
save_enabled(run_dir, remaining)
|
|
159
|
+
return {"status": "disabled", "name": name}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
# CLI dispatcher
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _resolve_run_dir(args) -> str | None:
|
|
168
|
+
"""Best-effort: resolve run_dir from args."""
|
|
169
|
+
from pathlib import Path
|
|
170
|
+
|
|
171
|
+
store_dir = getattr(args, "store_dir", None)
|
|
172
|
+
run_id_arg = getattr(args, "run", None)
|
|
173
|
+
if run_id_arg is None:
|
|
174
|
+
import os
|
|
175
|
+
|
|
176
|
+
run_id_arg = os.environ.get("ARCTX_RUN_ID")
|
|
177
|
+
if run_id_arg and store_dir:
|
|
178
|
+
return str(Path(store_dir) / run_id_arg)
|
|
179
|
+
if run_id_arg:
|
|
180
|
+
from arctx_cli.paths import resolve_store_dir
|
|
181
|
+
|
|
182
|
+
return str(Path(resolve_store_dir()) / run_id_arg)
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def cli_ext(args) -> int:
|
|
187
|
+
"""Dispatch arctx ext subcommands."""
|
|
188
|
+
cmd = args.ext_command
|
|
189
|
+
|
|
190
|
+
if cmd == "list":
|
|
191
|
+
run_dir = _resolve_run_dir(args)
|
|
192
|
+
result = run_ext_list_command(run_dir=run_dir)
|
|
193
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
if cmd == "show":
|
|
197
|
+
try:
|
|
198
|
+
result = run_ext_show_command(args.name)
|
|
199
|
+
except KeyError as exc:
|
|
200
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
201
|
+
return 1
|
|
202
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
203
|
+
return 0
|
|
204
|
+
|
|
205
|
+
if cmd == "enable":
|
|
206
|
+
run_dir = _resolve_run_dir(args)
|
|
207
|
+
if run_dir is None:
|
|
208
|
+
print("error: cannot resolve run directory. Pass --run and --store-dir.", file=sys.stderr)
|
|
209
|
+
return 1
|
|
210
|
+
try:
|
|
211
|
+
result = run_ext_enable_command(name=args.name, run_dir=run_dir)
|
|
212
|
+
except KeyError as exc:
|
|
213
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
214
|
+
return 1
|
|
215
|
+
status = result["status"]
|
|
216
|
+
if status == "already_enabled":
|
|
217
|
+
print(f"warning: {args.name!r} is already enabled", file=sys.stderr)
|
|
218
|
+
else:
|
|
219
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
220
|
+
return 0
|
|
221
|
+
|
|
222
|
+
if cmd == "disable":
|
|
223
|
+
run_dir = _resolve_run_dir(args)
|
|
224
|
+
if run_dir is None:
|
|
225
|
+
print("error: cannot resolve run directory. Pass --run and --store-dir.", file=sys.stderr)
|
|
226
|
+
return 1
|
|
227
|
+
try:
|
|
228
|
+
result = run_ext_disable_command(name=args.name, run_dir=run_dir)
|
|
229
|
+
except KeyError as exc:
|
|
230
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
231
|
+
return 1
|
|
232
|
+
print(f"warning: side effects of {args.name!r} are NOT reversed (files remain)", file=sys.stderr)
|
|
233
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
234
|
+
return 0
|
|
235
|
+
|
|
236
|
+
return 1
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""arctx git subcommand — attach commit hashes to transitions."""
|
|
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_cli.append_batch import graph_counts, maybe_append_or_save
|
|
11
|
+
from arctx_cli.context import (
|
|
12
|
+
resolve_run_id_from_args,
|
|
13
|
+
resolve_store,
|
|
14
|
+
resolve_user_id_from_args,
|
|
15
|
+
resolve_work_session_id_from_args,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Parser registration
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def add_parser(subparsers) -> argparse.ArgumentParser:
|
|
24
|
+
"""Register the ``git`` command namespace."""
|
|
25
|
+
git_parser = subparsers.add_parser("git", help="Git integration commands")
|
|
26
|
+
git_sub = git_parser.add_subparsers(dest="git_command", required=True)
|
|
27
|
+
|
|
28
|
+
from arctx_cli.ext.git.branch import add_parser as add_branch_parser
|
|
29
|
+
from arctx_cli.ext.git.cherry_pick import add_parser as add_cherry_pick_parser
|
|
30
|
+
from arctx_cli.ext.git.commit import add_parser as add_commit_parser
|
|
31
|
+
from arctx_cli.ext.git.hook import add_parser as add_hook_parser
|
|
32
|
+
from arctx_cli.ext.git.merge import add_parser as add_merge_parser
|
|
33
|
+
from arctx_cli.ext.git.reset import add_parser as add_reset_parser
|
|
34
|
+
from arctx_cli.ext.git.revert import add_parser as add_revert_parser
|
|
35
|
+
from arctx_cli.ext.git.verify import add_parser as add_verify_parser
|
|
36
|
+
from arctx_cli.ext.git.worktree import add_parser as add_worktree_parser
|
|
37
|
+
|
|
38
|
+
add_branch_parser(git_sub)
|
|
39
|
+
add_cherry_pick_parser(git_sub)
|
|
40
|
+
add_commit_parser(git_sub)
|
|
41
|
+
add_hook_parser(git_sub)
|
|
42
|
+
add_merge_parser(git_sub)
|
|
43
|
+
add_reset_parser(git_sub)
|
|
44
|
+
add_revert_parser(git_sub)
|
|
45
|
+
add_verify_parser(git_sub)
|
|
46
|
+
add_worktree_parser(git_sub)
|
|
47
|
+
|
|
48
|
+
sp_list = git_sub.add_parser("list", help="List git_change payloads for a Transition")
|
|
49
|
+
sp_list.add_argument("--transition", required=True, dest="transition_id")
|
|
50
|
+
sp_list.add_argument("--run", default=None)
|
|
51
|
+
sp_list.add_argument("--store-dir", default=None)
|
|
52
|
+
|
|
53
|
+
sp_add = git_sub.add_parser("add", help="Attach explicit Git commits to a Transition")
|
|
54
|
+
sp_add.add_argument("--transition", required=True, dest="transition_id")
|
|
55
|
+
sp_add.add_argument("--commit", action="append", required=True, dest="commits")
|
|
56
|
+
sp_add.add_argument("--run", default=None)
|
|
57
|
+
sp_add.add_argument("--store-dir", default=None)
|
|
58
|
+
sp_add.add_argument("--user", default=None)
|
|
59
|
+
sp_add.add_argument("--work-session", default=None)
|
|
60
|
+
|
|
61
|
+
sp_show = git_sub.add_parser("show", help="Show git_change payloads for a Transition")
|
|
62
|
+
sp_show.add_argument("--transition", required=True, dest="transition_id")
|
|
63
|
+
sp_show.add_argument("--run", default=None)
|
|
64
|
+
sp_show.add_argument("--store-dir", default=None)
|
|
65
|
+
|
|
66
|
+
return git_parser
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Helpers
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _run_dir(store: object, run_id: str) -> Path:
|
|
74
|
+
return store.run_path(run_id) # type: ignore[attr-defined]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Dispatch
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def cli_git(args) -> int:
|
|
83
|
+
"""Dispatch canonical ``arctx git`` subcommands."""
|
|
84
|
+
from arctx_cli.ext.git.branch import cli_branch
|
|
85
|
+
from arctx_cli.ext.git.cherry_pick import cli_cherry_pick
|
|
86
|
+
from arctx_cli.ext.git.commit import cli_commit
|
|
87
|
+
from arctx_cli.ext.git.hook import cli_hook
|
|
88
|
+
from arctx_cli.ext.git.merge import cli_merge
|
|
89
|
+
from arctx_cli.ext.git.reset import cli_reset
|
|
90
|
+
from arctx_cli.ext.git.revert import cli_revert
|
|
91
|
+
from arctx_cli.ext.git.verify import cli_verify
|
|
92
|
+
|
|
93
|
+
if args.git_command == "add":
|
|
94
|
+
return _cli_git_attach(args)
|
|
95
|
+
if args.git_command == "branch":
|
|
96
|
+
return cli_branch(args)
|
|
97
|
+
if args.git_command == "cherry-pick":
|
|
98
|
+
return cli_cherry_pick(args)
|
|
99
|
+
if args.git_command == "commit":
|
|
100
|
+
return cli_commit(args)
|
|
101
|
+
if args.git_command == "hook":
|
|
102
|
+
return cli_hook(args)
|
|
103
|
+
if args.git_command == "list":
|
|
104
|
+
return _cli_git_list(args)
|
|
105
|
+
if args.git_command == "merge":
|
|
106
|
+
return cli_merge(args)
|
|
107
|
+
if args.git_command == "reset":
|
|
108
|
+
return cli_reset(args)
|
|
109
|
+
if args.git_command == "revert":
|
|
110
|
+
return cli_revert(args)
|
|
111
|
+
if args.git_command == "show":
|
|
112
|
+
return _cli_git_show(args)
|
|
113
|
+
if args.git_command == "verify":
|
|
114
|
+
return cli_verify(args)
|
|
115
|
+
if args.git_command == "worktree":
|
|
116
|
+
from arctx_cli.ext.git.worktree import cli_worktree # noqa: PLC0415
|
|
117
|
+
|
|
118
|
+
return cli_worktree(args)
|
|
119
|
+
print(f"unknown git subcommand: {args.git_command}", file=sys.stderr)
|
|
120
|
+
return 1
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# attach
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _git_payloads_for_transition(args) -> tuple[object, list]:
|
|
129
|
+
store = resolve_store(args.store_dir)
|
|
130
|
+
run_id = resolve_run_id_from_args(args)
|
|
131
|
+
if not store.run_path(run_id).exists():
|
|
132
|
+
raise KeyError(f"unknown run_id: {run_id}")
|
|
133
|
+
handle = store.load_run(run_id)
|
|
134
|
+
if args.transition_id not in handle.run_graph.transitions:
|
|
135
|
+
raise KeyError(f"unknown transition_id: {args.transition_id}")
|
|
136
|
+
payloads = handle.run_graph.payloads_for_transition(
|
|
137
|
+
args.transition_id,
|
|
138
|
+
payload_type="git_change",
|
|
139
|
+
)
|
|
140
|
+
return handle, payloads
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _cli_git_list(args) -> int:
|
|
144
|
+
try:
|
|
145
|
+
_, payloads = _git_payloads_for_transition(args)
|
|
146
|
+
except KeyError as exc:
|
|
147
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
148
|
+
return 1
|
|
149
|
+
|
|
150
|
+
commits: list[str] = []
|
|
151
|
+
for payload in payloads:
|
|
152
|
+
for entry in getattr(payload, "commit_log", ()):
|
|
153
|
+
sha = getattr(entry, "sha", None)
|
|
154
|
+
if sha is not None:
|
|
155
|
+
commits.append(str(sha))
|
|
156
|
+
print(
|
|
157
|
+
json.dumps(
|
|
158
|
+
{
|
|
159
|
+
"transition_id": args.transition_id,
|
|
160
|
+
"commits": commits,
|
|
161
|
+
},
|
|
162
|
+
ensure_ascii=False,
|
|
163
|
+
indent=2,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
return 0
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _cli_git_show(args) -> int:
|
|
170
|
+
try:
|
|
171
|
+
_, payloads = _git_payloads_for_transition(args)
|
|
172
|
+
except KeyError as exc:
|
|
173
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
174
|
+
return 1
|
|
175
|
+
print(json.dumps([p.to_dict() for p in payloads], ensure_ascii=False, indent=2))
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _cli_git_attach(args) -> int:
|
|
180
|
+
store = resolve_store(args.store_dir)
|
|
181
|
+
run_id = resolve_run_id_from_args(args)
|
|
182
|
+
user_id = resolve_user_id_from_args(args)
|
|
183
|
+
work_session_id = resolve_work_session_id_from_args(args)
|
|
184
|
+
|
|
185
|
+
if not store.run_path(run_id).exists():
|
|
186
|
+
print(f"error: unknown run_id: {run_id}", file=sys.stderr)
|
|
187
|
+
return 1
|
|
188
|
+
|
|
189
|
+
handle = store.load_run(run_id)
|
|
190
|
+
run_dir = _run_dir(store, run_id)
|
|
191
|
+
|
|
192
|
+
from arctx.ext.git.helpers.attach import attach_commits_to_transition
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
before = graph_counts(handle)
|
|
196
|
+
result = attach_commits_to_transition(
|
|
197
|
+
handle,
|
|
198
|
+
run_dir,
|
|
199
|
+
args.transition_id,
|
|
200
|
+
tuple(args.commits),
|
|
201
|
+
user_id=user_id,
|
|
202
|
+
work_session_id=work_session_id,
|
|
203
|
+
)
|
|
204
|
+
except (KeyError, ValueError) as exc:
|
|
205
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
206
|
+
return 1
|
|
207
|
+
|
|
208
|
+
maybe_append_or_save(
|
|
209
|
+
store=store,
|
|
210
|
+
handle=handle,
|
|
211
|
+
user_id=user_id,
|
|
212
|
+
work_session_id=work_session_id,
|
|
213
|
+
before=before,
|
|
214
|
+
)
|
|
215
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
216
|
+
return 0
|