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