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,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