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,73 @@
1
+ """arctx graph commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+
8
+ from arctx_cli.commands.dump import run_dump_command
9
+ from arctx_cli.commands.reachable import run_reachable_command
10
+ from arctx_cli.commands.trace import run_trace_command
11
+ from arctx_cli.context import resolve_run_id_from_args
12
+
13
+
14
+ def add_parser(subparsers) -> argparse.ArgumentParser:
15
+ parser = subparsers.add_parser("graph", help="Inspect graph structure")
16
+ graph_sub = parser.add_subparsers(dest="graph_command", required=True)
17
+
18
+ sp_dump = graph_sub.add_parser("dump", help="Render the graph")
19
+ sp_dump.add_argument("--format", dest="fmt", choices=["outline", "mermaid"], default="outline")
20
+ sp_dump.add_argument("--node", dest="node_id", default=None)
21
+ sp_dump.add_argument("--depth", type=int, default=None)
22
+ sp_dump.add_argument("--full-payloads", action="store_true")
23
+ sp_dump.add_argument("--run", default=None)
24
+ sp_dump.add_argument("--store-dir", default=None)
25
+
26
+ sp_trace = graph_sub.add_parser("trace", help="Trace history from a node")
27
+ sp_trace.add_argument("node_id")
28
+ sp_trace.add_argument("--depth", type=int, default=None)
29
+ sp_trace.add_argument("--run", default=None)
30
+ sp_trace.add_argument("--store-dir", default=None)
31
+
32
+ sp_reachable = graph_sub.add_parser("reachable", help="Show active graph forward from a node")
33
+ sp_reachable.add_argument("node_id")
34
+ sp_reachable.add_argument("--include-records", action="store_true")
35
+ sp_reachable.add_argument("--run", default=None)
36
+ sp_reachable.add_argument("--store-dir", default=None)
37
+
38
+ return parser
39
+
40
+
41
+ def cli_graph(args) -> int:
42
+ if args.graph_command == "dump":
43
+ print(
44
+ run_dump_command(
45
+ run_id=resolve_run_id_from_args(args),
46
+ fmt=args.fmt,
47
+ store_dir=args.store_dir,
48
+ node_id=args.node_id,
49
+ depth=args.depth,
50
+ full_payloads=args.full_payloads,
51
+ )
52
+ )
53
+ return 0
54
+ if args.graph_command == "trace":
55
+ result = run_trace_command(
56
+ run_id=resolve_run_id_from_args(args),
57
+ from_node_id=args.node_id,
58
+ depth=args.depth,
59
+ store_dir=args.store_dir,
60
+ )
61
+ print(json.dumps(result["history"], ensure_ascii=False, indent=2))
62
+ return 0
63
+ if args.graph_command == "reachable":
64
+ result = run_reachable_command(
65
+ run_id=resolve_run_id_from_args(args),
66
+ from_node=args.node_id,
67
+ view_name=None,
68
+ include_records=args.include_records,
69
+ store_dir=args.store_dir,
70
+ )
71
+ print(json.dumps(result, ensure_ascii=False, indent=2))
72
+ return 0
73
+ return 1
@@ -0,0 +1,360 @@
1
+ """arctx CLI guide command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ TOPICS_EN: dict[str, str] = {
9
+ "overview": """\
10
+ # arctx guide
11
+
12
+ arctx records optimization and problem-solving work as an append-only RunGraph DAG.
13
+
14
+ The graph skeleton has two record types:
15
+
16
+ - Node: a state or point in the work history.
17
+ - Transition: a step from one or more input nodes to exactly one output node.
18
+
19
+ Meaning is attached as payloads. TransitionPayload, NodePayload,
20
+ GitChangePayload, and CutPayload explain what a node or transition means.
21
+
22
+ Basic loop:
23
+
24
+ ```text
25
+ init -> transition -> dump
26
+ ```
27
+
28
+ Common commands:
29
+
30
+ ```bash
31
+ arctx init req_demo --run-id demo
32
+ eval "$(arctx work-session env --run demo --new)"
33
+ arctx transition create --run demo --from <node_id> \\
34
+ --payload-type transition_payload --field type=experiment --field name=baseline
35
+ arctx payload add --run demo --node <node_id> \\
36
+ --payload-type node_payload --field type=note --field text="context here"
37
+ arctx graph dump --run demo
38
+ ```
39
+ """,
40
+ "agent": """\
41
+ # Agent Rules
42
+
43
+ - Treat Node and Transition IDs as opaque.
44
+ - Put domain meaning in payloads (TransitionPayload / NodePayload), not in graph fields.
45
+ - Use `arctx transition create` to record attempts.
46
+ - Use `arctx payload add` for node or transition annotations.
47
+ - Use `arctx graph dump` when you need broad context.
48
+ - Use CutPayload through `arctx cut` instead of deleting records.
49
+ - For parallel work, pin each process to a distinct work session with
50
+ `arctx work-session env --run <run_id> --new` or pass `--work-session` explicitly.
51
+ """,
52
+ "work-session": """\
53
+ # Work Sessions
54
+
55
+ Use a work session to separate one process, terminal, or worker's history inside
56
+ the same run.
57
+
58
+ There are two supported modes.
59
+
60
+ Explicit mode passes the session on every mutating command:
61
+
62
+ ```bash
63
+ arctx transition create --run demo --work-session ws_a --from <node_id> \\
64
+ --payload-type transition_payload --field type=experiment
65
+ ```
66
+
67
+ Fixed mode stores the run/session in the current shell environment, not in
68
+ the shared `<gitdir>/arctx-id` pointer:
69
+
70
+ ```bash
71
+ eval "$(arctx work-session env --run demo --new)"
72
+ arctx transition create --from <node_id> --payload-type transition_payload
73
+ ```
74
+
75
+ For sub processes, use `spawn`; each invocation creates or uses one session and
76
+ passes `ARCTX_RUN_ID`, `ARCTX_WORK_SESSION_ID`, and `ARCTX_USER_ID` only to the child.
77
+
78
+ ```bash
79
+ arctx work-session spawn --run demo -- codex
80
+ arctx work-session spawn --run demo -- claude
81
+ ```
82
+ """,
83
+ "dump": """\
84
+ # Dump
85
+
86
+ `arctx graph dump` renders the active graph in outline or mermaid form. Each
87
+ Transition has exactly one output Node. Cut records render with `✂`; revisited
88
+ nodes with `↻`.
89
+ """,
90
+ "record": """\
91
+ # Record One Experiment
92
+
93
+ ```bash
94
+ arctx init req_demo --run-id demo
95
+ arctx transition create --run demo --from <root_node_id> \\
96
+ --payload-type transition_payload --field type=experiment --field name=run1
97
+ arctx transition show --run demo <transition_id> --with-payloads
98
+ ```
99
+ """,
100
+ "payloads": """\
101
+ # Payloads
102
+
103
+ - TransitionPayload attaches to a Transition. Use `type` to describe the kind of step.
104
+ - NodePayload attaches to a Node. Use `type` to describe the kind of annotation.
105
+ - GitChangePayload attaches to a Transition with git commit / diff info.
106
+ - CutPayload attaches to a Node or Transition to mark it inactive.
107
+
108
+ Custom subclasses of PayloadBase can be registered with `register_payload_class()`.
109
+ """,
110
+ "cut": """\
111
+ # Cut
112
+
113
+ `arctx cut --node <node_id>` or `arctx cut --transition <transition_id>` appends
114
+ an append-only CutPayload. Records are not deleted; inactive branches are computed at read time.
115
+ """,
116
+ "joins": """\
117
+ # Joins (Multi-input Transitions)
118
+
119
+ Pass multiple `--from` values to `arctx transition create` to create a
120
+ Transition with multiple input nodes but still exactly one output node.
121
+ """,
122
+ "git": """\
123
+ # Git
124
+
125
+ Git commands attach commit information to a Transition.
126
+
127
+ ```bash
128
+ arctx git add --run demo --transition <transition_id> --commit <sha>
129
+ arctx git list --run demo --transition <transition_id>
130
+ arctx git show --run demo --transition <transition_id>
131
+ ```
132
+ """,
133
+ }
134
+
135
+
136
+ TOPICS_JA: dict[str, str] = {
137
+ "overview": """\
138
+ # arctx guide
139
+
140
+ arctx は作業履歴を append-only な DAG として記録します。
141
+
142
+ グラフ骨格はこの 2 種類だけです。
143
+
144
+ - Node: 作業履歴上の状態や地点。
145
+ - Transition: 1 つ以上の Node から 1 つの output Node への作業ステップ。
146
+
147
+ 意味は payload に分離します。TransitionPayload / NodePayload /
148
+ GitChangePayload / CutPayload が、Node や Transition に意味を付けます。
149
+
150
+ 基本ループ:
151
+
152
+ ```text
153
+ init -> transition -> dump
154
+ ```
155
+
156
+ よく使うコマンド:
157
+
158
+ ```bash
159
+ arctx init req_demo --run-id demo
160
+ eval "$(arctx work-session env --run demo --new)"
161
+ arctx transition create --run demo --from <node_id> \\
162
+ --payload-type transition_payload --field type=experiment --field name=baseline
163
+ arctx payload add --run demo --node <node_id> \\
164
+ --payload-type node_payload --field type=note --field text="context here"
165
+ arctx graph dump --run demo
166
+ ```
167
+ """,
168
+ "agent": """\
169
+ # Agent Rules
170
+
171
+ - Node / Transition ID は opaque として扱う。
172
+ - ドメイン上の意味は graph record ではなく payload に入れる。
173
+ - 作業の記録は `arctx transition create` を使う。
174
+ - Node / Transition への注釈は `arctx payload add` を使う。
175
+ - 広い文脈確認は `arctx graph dump` を使う。
176
+ - 履歴は削除せず、`arctx cut` で CutPayload を追加する。
177
+ - 並列作業では `arctx work-session env --run <run_id> --new` でプロセスごとに
178
+ 別 work session を固定するか、毎回 `--work-session` を明示する。
179
+ """,
180
+ "work-session": """\
181
+ # Work Sessions
182
+
183
+ work session は、同じ run の中で、1 つのプロセス・ターミナル・worker の履歴を
184
+ 分けるための単位です。
185
+
186
+ 使い方は 2 種類あります。
187
+
188
+ 毎回明示モードでは、mutating command ごとに session を渡します。
189
+
190
+ ```bash
191
+ arctx transition create --run demo --work-session ws_a --from <node_id> \\
192
+ --payload-type transition_payload --field type=experiment
193
+ ```
194
+
195
+ 固定モードでは、共有の `<gitdir>/arctx-id` ではなく、現在の shell 環境に run/session
196
+ を固定します。並列ターミナルや sub process ではこちらを使います。
197
+
198
+ ```bash
199
+ eval "$(arctx work-session env --run demo --new)"
200
+ arctx transition create --from <node_id> --payload-type transition_payload
201
+ ```
202
+
203
+ sub process を起動する場合は `spawn` を使います。各 invocation は 1 つの session
204
+ を作成または使用し、子プロセスだけに `ARCTX_RUN_ID` / `ARCTX_WORK_SESSION_ID` /
205
+ `ARCTX_USER_ID` を渡します。
206
+
207
+ ```bash
208
+ arctx work-session spawn --run demo -- codex
209
+ arctx work-session spawn --run demo -- claude
210
+ ```
211
+ """,
212
+ "dump": """\
213
+ # Dump
214
+
215
+ `arctx graph dump` は active graph を outline または mermaid 形式で表示します。
216
+ 各 Transition は必ず 1 つの output Node を持ちます。Cut 済みレコードは `✂`、
217
+ 再訪した Node は `↻` で表示されます。
218
+ """,
219
+ "record": """\
220
+ # 1 つの実験を記録する
221
+
222
+ ```bash
223
+ arctx init req_demo --run-id demo
224
+ arctx transition create --run demo --from <root_node_id> \\
225
+ --payload-type transition_payload --field type=experiment --field name=run1
226
+ arctx transition show --run demo <transition_id> --with-payloads
227
+ ```
228
+ """,
229
+ "payloads": """\
230
+ # Payloads
231
+
232
+ - TransitionPayload は Transition に付けます。`type` で作業ステップの種類を表します。
233
+ - NodePayload は Node に付けます。`type` で注釈の種類を表します。
234
+ - GitChangePayload は Transition に付け、commit 情報を保持します。
235
+ - CutPayload は Node または Transition に付け、inactive として扱うために使います。
236
+
237
+ 独自の PayloadBase サブクラスは `register_payload_class()` で登録できます。
238
+ """,
239
+ "cut": """\
240
+ # Cut
241
+
242
+ `arctx cut node <node_id>` または `arctx cut transition <transition_id>` は
243
+ append-only な CutPayload を追加します。レコードは削除されません。inactive な
244
+ branch は読み取り時に計算されます。
245
+ """,
246
+ "joins": """\
247
+ # Joins (Multi-input Transitions)
248
+
249
+ `arctx transition create` に複数の `--from` を渡すと、複数 input node から
250
+ 1 つの output node へ向かう Transition を作成できます。
251
+ """,
252
+ "git": """\
253
+ # Git
254
+
255
+ Git コマンドは Transition に commit 情報を付けます。
256
+
257
+ ```bash
258
+ arctx git add --run demo --transition <transition_id> --commit <sha>
259
+ arctx git list --run demo --transition <transition_id>
260
+ arctx git show --run demo --transition <transition_id>
261
+ ```
262
+ """,
263
+ }
264
+
265
+
266
+ GUIDES: dict[str, dict[str, str]] = {
267
+ "ja": TOPICS_JA,
268
+ "en": TOPICS_EN,
269
+ }
270
+
271
+ TOPIC_SUMMARIES: dict[str, dict[str, str]] = {
272
+ "en": {
273
+ "overview": "Concept, RunGraph model, basic loop",
274
+ "agent": "Rules for agents using arctx",
275
+ "dump": "arctx graph dump output model",
276
+ "record": "Typical workflow to record one experiment",
277
+ "work-session": "Separate parallel process history within one run",
278
+ "payloads": "Payload types and attachment targets",
279
+ "cut": "Append-only invalidation",
280
+ "joins": "Multi-input transitions",
281
+ "git": "Record Git commits on a Transition",
282
+ },
283
+ "ja": {
284
+ "overview": "概念、RunGraph model、基本ループ",
285
+ "agent": "arctx を使う agent 向けルール",
286
+ "dump": "arctx graph dump の出力モデル",
287
+ "record": "1 つの実験を記録する基本フロー",
288
+ "work-session": "同じ run 内で並列プロセスの履歴を分ける",
289
+ "payloads": "Payload の種類と attachment target",
290
+ "cut": "Append-only な無効化",
291
+ "joins": "複数 input node の Transition",
292
+ "git": "Transition への Git commit 記録",
293
+ },
294
+ }
295
+
296
+ _DEFAULT_TOPIC = "overview"
297
+
298
+
299
+ def run_guide_command(*, lang: str = "en", topic: str = _DEFAULT_TOPIC) -> dict:
300
+ """Return the guide text for *topic* in *lang*."""
301
+ topics = GUIDES[lang]
302
+ if topic not in topics:
303
+ valid = ", ".join(sorted(topics))
304
+ raise ValueError(f"Unknown topic {topic!r}. Valid topics: {valid}")
305
+ return {"guide": topics[topic], "lang": lang, "topic": topic}
306
+
307
+
308
+ def run_guide_list(lang: str = "en") -> dict:
309
+ """Return topic id + summary pairs for *lang*."""
310
+ topics = GUIDES[lang]
311
+ summaries = TOPIC_SUMMARIES[lang]
312
+ return {
313
+ "topics": [{"id": tid, "summary": summaries.get(tid, "")} for tid in topics],
314
+ "lang": lang,
315
+ }
316
+
317
+
318
+ def add_parser(subparsers) -> argparse.ArgumentParser:
319
+ parser = subparsers.add_parser(
320
+ "guide",
321
+ help="Show the arctx concept and CLI workflow guide",
322
+ )
323
+ parser.add_argument(
324
+ "--lang",
325
+ choices=sorted(GUIDES),
326
+ default="en",
327
+ help="Guide language (default: en)",
328
+ )
329
+ parser.add_argument(
330
+ "--topic",
331
+ default=None,
332
+ metavar="NAME",
333
+ help="Show a specific subtopic (see --list for names)",
334
+ )
335
+ parser.add_argument(
336
+ "--list",
337
+ action="store_true",
338
+ dest="list_topics",
339
+ help="List available topic names and descriptions",
340
+ )
341
+ return parser
342
+
343
+
344
+ def cli_guide(args) -> int:
345
+ if args.list_topics:
346
+ result = run_guide_list(lang=args.lang)
347
+ for entry in result["topics"]:
348
+ print(f" {entry['id']:<12} {entry['summary']}")
349
+ return 0
350
+
351
+ topic = args.topic if args.topic is not None else _DEFAULT_TOPIC
352
+ try:
353
+ result = run_guide_command(lang=args.lang, topic=topic)
354
+ except ValueError as exc:
355
+ print(f"error: {exc}", file=sys.stderr)
356
+ print("Run `arctx guide --list` to see available topics.", file=sys.stderr)
357
+ return 1
358
+
359
+ print(result["guide"])
360
+ return 0
@@ -0,0 +1,223 @@
1
+ """arctx CLI init command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ import arctx as arctx
8
+ from arctx_cli.context import resolve_store
9
+ from arctx_cli.paths import find_repo_root, resolve_store_dir, arctx_id_path, write_arctx_id
10
+ from arctx.core.schema.requirements import Requirement
11
+
12
+
13
+ def add_parser(subparsers) -> argparse.ArgumentParser:
14
+ """Register the ``init`` subcommand parser."""
15
+ parser = subparsers.add_parser("init", help="Initialize a new run")
16
+ parser.add_argument("requirement_id", help="Requirement identifier")
17
+ parser.add_argument(
18
+ "--target-type",
19
+ default="code",
20
+ help="Target category (default: code)",
21
+ )
22
+ parser.add_argument(
23
+ "--target-id",
24
+ default=None,
25
+ help="Specific target identifier (default: requirement_id)",
26
+ )
27
+ parser.add_argument(
28
+ "--run-id",
29
+ default=None,
30
+ help="Explicit run id (default: auto-generated)",
31
+ )
32
+ parser.add_argument(
33
+ "--store-dir",
34
+ default=None,
35
+ help="Directory to save runs (default: <ARCTX_HOME>/runs)",
36
+ )
37
+ parser.add_argument(
38
+ "--no-hooks",
39
+ action="store_true",
40
+ help="Skip installing git hooks (post-rewrite etc.)",
41
+ )
42
+ parser.add_argument(
43
+ "--extension",
44
+ action="append",
45
+ default=[],
46
+ dest="extension",
47
+ metavar="NAME",
48
+ help="Enable extension by name (may be repeated)",
49
+ )
50
+
51
+ # Pre-register init options from all built-in extensions so that
52
+ # extension-specific flags (e.g. --dummy-flag) are accepted at parse time.
53
+ from arctx.ext import list_available, load_extension # noqa: PLC0415
54
+
55
+ for ext_name in list_available():
56
+ try:
57
+ ext = load_extension(ext_name)
58
+ ext.register_init_options(parser)
59
+ except Exception: # noqa: BLE001
60
+ continue
61
+
62
+ return parser
63
+
64
+
65
+ def run_init_command(
66
+ *,
67
+ requirement_id: str,
68
+ target_type: str,
69
+ target_id: str | None,
70
+ run_id: str | None,
71
+ store_dir: str | None,
72
+ no_hooks: bool = False,
73
+ extensions: list[str] | None = None,
74
+ extension_options: dict[str, object] | None = None,
75
+ ) -> dict[str, str]:
76
+ """Create a new run and save it to disk.
77
+
78
+ Parameters
79
+ ----------
80
+ requirement_id:
81
+ Identifier for the requirement.
82
+ target_type:
83
+ Category of the target (e.g. "code", "kernel").
84
+ target_id:
85
+ Specific target identifier.
86
+ run_id:
87
+ Explicit run id. If None, one is generated automatically.
88
+ store_dir:
89
+ Directory under which run directories are created.
90
+ If None, defaults to ``<ARCTX_HOME>/runs``.
91
+ no_hooks:
92
+ If True, skip installing git hooks.
93
+ extensions:
94
+ Names of extensions to enable. Each extension's ``on_init`` is called
95
+ and the extension is recorded in ``<run_dir>/extensions.json``.
96
+ extension_options:
97
+ Flat dict of parsed argparse values for extension-specific options,
98
+ keyed by their ``dest`` names (e.g. ``ext__dummy_dummy_flag``).
99
+
100
+ Returns
101
+ -------
102
+ dict with at least ``run_id``, ``root_node_id``, and ``arctx_id_path``
103
+ (the path where the active-run pointer was written under ``<gitdir>/``,
104
+ or None if not in a git repo).
105
+
106
+ Raises
107
+ ------
108
+ FileExistsError
109
+ If the run directory already exists.
110
+ KeyError
111
+ If an extension name is not in the built-in registry.
112
+ """
113
+ resolved_store_dir = store_dir if store_dir is not None else resolve_store_dir()
114
+
115
+ requirement = Requirement(
116
+ requirement_id=requirement_id,
117
+ target_type=target_type,
118
+ target_id=target_id or requirement_id,
119
+ )
120
+
121
+ handle = arctx.init(requirement, run_id=run_id)
122
+
123
+ store = resolve_store(resolved_store_dir)
124
+ run_path = store.run_path(handle.run_id)
125
+ if run_path.exists():
126
+ raise FileExistsError(f"run directory already exists: {run_path}")
127
+
128
+ store.save_run(handle)
129
+
130
+ # Write the active-run pointer under <gitdir>/arctx-id if we are
131
+ # inside a git repo. Living under .git/ means git itself never
132
+ # tracks it, so there is no risk of accidental commits.
133
+ written_arctx_id_path: str | None = None
134
+ try:
135
+ repo_root = find_repo_root()
136
+ write_arctx_id(repo_root, handle.run_id)
137
+ written_arctx_id_path = str(arctx_id_path(repo_root))
138
+ except RuntimeError:
139
+ # Not inside a git repo — skip pointer creation silently.
140
+ pass
141
+
142
+ installed_hook_path: str | None = None
143
+ hook_warning: str | None = None
144
+
145
+ # Activate requested extensions.
146
+ enabled_extensions: list[str] = []
147
+ if extensions:
148
+ from arctx.ext import load_extension # noqa: PLC0415
149
+ from arctx.ext.base import InitContext # noqa: PLC0415
150
+ from arctx.ext.enabled import EnabledExtension, add_enabled # noqa: PLC0415
151
+
152
+ for ext_name in extensions:
153
+ ext = load_extension(ext_name) # raises KeyError for unknown names
154
+ opts = dict(extension_options or {})
155
+ if no_hooks:
156
+ opts["ext_git_no_hooks"] = True
157
+ git_hook_existed = False
158
+ if ext.name == "git" and not opts.get("ext_git_no_hooks"):
159
+ try:
160
+ git_hook_existed = (
161
+ find_repo_root() / ".git" / "hooks" / "post-rewrite"
162
+ ).exists()
163
+ except RuntimeError:
164
+ git_hook_existed = False
165
+ ctx = InitContext(
166
+ run_id=handle.run_id,
167
+ run_dir=str(run_path),
168
+ options=opts,
169
+ )
170
+ ext.on_init(ctx)
171
+ if ext.name == "git" and not opts.get("ext_git_no_hooks"):
172
+ try:
173
+ hook_path = find_repo_root() / ".git" / "hooks" / "post-rewrite"
174
+ if hook_path.exists() and not git_hook_existed:
175
+ installed_hook_path = str(hook_path)
176
+ elif hook_path.exists() and git_hook_existed:
177
+ hook_warning = f"hook already exists: {hook_path}"
178
+ except RuntimeError:
179
+ pass
180
+ add_enabled(run_path, EnabledExtension(name=ext.name, version=ext.version, config={}))
181
+ enabled_extensions.append(ext_name)
182
+
183
+ return {
184
+ "run_id": handle.run_id,
185
+ "root_node_id": handle.root_node_id,
186
+ "store_dir": resolved_store_dir,
187
+ "arctx_id_path": written_arctx_id_path,
188
+ "hook_path": installed_hook_path,
189
+ "hook_warning": hook_warning,
190
+ "enabled_extensions": enabled_extensions,
191
+ }
192
+
193
+
194
+ def cli_init(args) -> int:
195
+ """Entry point for ``arctx init`` subcommand.
196
+
197
+ Prints the generated run_id to stdout on success.
198
+ """
199
+ import sys
200
+
201
+ try:
202
+ result = run_init_command(
203
+ requirement_id=args.requirement_id,
204
+ target_type=args.target_type,
205
+ target_id=args.target_id,
206
+ run_id=args.run_id,
207
+ store_dir=args.store_dir,
208
+ no_hooks=args.no_hooks,
209
+ extensions=list(args.extension or []),
210
+ extension_options=vars(args),
211
+ )
212
+ except KeyError as exc:
213
+ print(f"error: {exc}", file=sys.stderr)
214
+ return 1
215
+ print(result["run_id"])
216
+ if result.get("hook_path"):
217
+ print(
218
+ f"hint: git hook installed at {result['hook_path']}",
219
+ file=sys.stderr,
220
+ )
221
+ if result.get("hook_warning"):
222
+ print(f"warning: {result['hook_warning']}", file=sys.stderr)
223
+ return 0
@@ -0,0 +1,45 @@
1
+ """arctx CLI list command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+
8
+ from arctx_cli.context import resolve_store
9
+
10
+
11
+ def add_parser(subparsers) -> argparse.ArgumentParser:
12
+ """Register the ``list`` subcommand parser."""
13
+ parser = subparsers.add_parser("list", help="List saved runs")
14
+ parser.add_argument(
15
+ "--store-dir",
16
+ default=None,
17
+ help="Directory where runs are stored (default: .arctx/runs)",
18
+ )
19
+ return parser
20
+
21
+
22
+ def run_list_command(*, store_dir: str) -> dict:
23
+ """List all runs in the store directory.
24
+
25
+ Parameters
26
+ ----------
27
+ store_dir:
28
+ Directory where runs are stored.
29
+
30
+ Returns
31
+ -------
32
+ dict with ``runs`` key containing a list of run summary dicts.
33
+ """
34
+ store = resolve_store(store_dir)
35
+ return {"runs": store.list_runs()}
36
+
37
+
38
+ def cli_list(args) -> int:
39
+ """Entry point for ``arctx list`` subcommand.
40
+
41
+ Prints the list of runs as JSON to stdout.
42
+ """
43
+ result = run_list_command(store_dir=args.store_dir)
44
+ print(json.dumps(result["runs"], ensure_ascii=False, indent=2))
45
+ return 0