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
arctx_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""arctx CLI package."""
|
arctx_cli/alias.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Alias resolution for arctx CLI.
|
|
2
|
+
|
|
3
|
+
Resolution priority (later wins for the same key when merging):
|
|
4
|
+
1. Extension default_aliases (load order; first ext wins among ext-default conflicts)
|
|
5
|
+
2. User config (~/.config/arctx/aliases.toml)
|
|
6
|
+
3. Run-local config (<run_dir>/aliases.toml)
|
|
7
|
+
|
|
8
|
+
Alias expansion is one-level only — alias-to-alias chains are prohibited to
|
|
9
|
+
prevent infinite loops.
|
|
10
|
+
|
|
11
|
+
Format of aliases.toml::
|
|
12
|
+
|
|
13
|
+
[aliases]
|
|
14
|
+
commit = "git commit"
|
|
15
|
+
c = "git commit"
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import tomllib # py311+ stdlib
|
|
26
|
+
except ModuleNotFoundError: # py310 fallback
|
|
27
|
+
try:
|
|
28
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
29
|
+
except ModuleNotFoundError as exc:
|
|
30
|
+
raise ImportError(
|
|
31
|
+
"Python 3.10 requires the 'tomli' package for TOML parsing. "
|
|
32
|
+
"Install it with: pip install tomli"
|
|
33
|
+
) from exc
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Path helpers
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _user_alias_path() -> Path:
|
|
42
|
+
"""Return ``~/.config/arctx/aliases.toml``."""
|
|
43
|
+
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
44
|
+
if xdg:
|
|
45
|
+
return Path(xdg) / "arctx" / "aliases.toml"
|
|
46
|
+
return Path.home() / ".config" / "arctx" / "aliases.toml"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _run_alias_path(run_dir: Path | str) -> Path:
|
|
50
|
+
"""Return ``<run_dir>/aliases.toml``."""
|
|
51
|
+
return Path(run_dir) / "aliases.toml"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# TOML writer (minimal — stdlib has no tomllib writer)
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _write_toml_aliases(path: Path, aliases: dict[str, str]) -> None:
|
|
60
|
+
"""Write *aliases* to *path* as a ``[aliases]`` TOML section.
|
|
61
|
+
|
|
62
|
+
Entries are sorted for stable output.
|
|
63
|
+
"""
|
|
64
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
lines = ["[aliases]\n"]
|
|
66
|
+
for key in sorted(aliases.keys()):
|
|
67
|
+
value = aliases[key].replace("\\", "\\\\").replace('"', '\\"')
|
|
68
|
+
lines.append(f'{key} = "{value}"\n')
|
|
69
|
+
path.write_text("".join(lines), encoding="utf-8")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _read_toml_aliases(path: Path) -> dict[str, str]:
|
|
73
|
+
"""Read the ``[aliases]`` table from *path*. Returns {} if missing."""
|
|
74
|
+
if not path.exists():
|
|
75
|
+
return {}
|
|
76
|
+
with path.open("rb") as fh:
|
|
77
|
+
data = tomllib.load(fh)
|
|
78
|
+
raw = data.get("aliases", {})
|
|
79
|
+
return {str(k): str(v) for k, v in raw.items()}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Public API
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def load_alias_table(
|
|
88
|
+
*,
|
|
89
|
+
run_dir: str | Path | None = None,
|
|
90
|
+
extensions_default_aliases: Optional[list[dict[str, str]]] = None,
|
|
91
|
+
) -> dict[str, str]:
|
|
92
|
+
"""Build the merged alias table.
|
|
93
|
+
|
|
94
|
+
Priority (later entries win for the same key):
|
|
95
|
+
- Extension defaults (in load order; first ext wins for ext-level conflicts)
|
|
96
|
+
- User config (~/.config/arctx/aliases.toml)
|
|
97
|
+
- Run-local config (<run_dir>/aliases.toml)
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
run_dir:
|
|
102
|
+
Directory of the active run. When provided, ``<run_dir>/aliases.toml``
|
|
103
|
+
is loaded and takes highest priority.
|
|
104
|
+
extensions_default_aliases:
|
|
105
|
+
List of ``default_aliases()`` dicts from enabled extensions, in load
|
|
106
|
+
order. First ext wins for duplicate alias names at this tier.
|
|
107
|
+
"""
|
|
108
|
+
merged: dict[str, str] = {}
|
|
109
|
+
|
|
110
|
+
# 1. Extension defaults (first ext wins → iterate in order, skip if already set)
|
|
111
|
+
if extensions_default_aliases:
|
|
112
|
+
for ext_aliases in extensions_default_aliases:
|
|
113
|
+
for name, target in ext_aliases.items():
|
|
114
|
+
if name not in merged:
|
|
115
|
+
merged[name] = target
|
|
116
|
+
|
|
117
|
+
# 2. User config (overrides ext defaults)
|
|
118
|
+
user_aliases = _read_toml_aliases(_user_alias_path())
|
|
119
|
+
merged.update(user_aliases)
|
|
120
|
+
|
|
121
|
+
# 3. Run-local config (highest priority)
|
|
122
|
+
if run_dir is not None:
|
|
123
|
+
run_aliases = _read_toml_aliases(_run_alias_path(run_dir))
|
|
124
|
+
merged.update(run_aliases)
|
|
125
|
+
|
|
126
|
+
return merged
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def resolve_alias(alias_table: dict[str, str], tokens: list[str]) -> list[str]:
|
|
130
|
+
"""Expand *tokens[0]* if it appears in *alias_table*; else return *tokens*.
|
|
131
|
+
|
|
132
|
+
Expansion is one-level only. The alias value is split on whitespace (no
|
|
133
|
+
shell quoting support needed for now) and prepended to the remaining tokens.
|
|
134
|
+
|
|
135
|
+
Examples
|
|
136
|
+
--------
|
|
137
|
+
>>> resolve_alias({"commit": "git commit"}, ["commit", "-m", "x"])
|
|
138
|
+
["git", "commit", "-m", "x"]
|
|
139
|
+
>>> resolve_alias({}, ["init", "req"])
|
|
140
|
+
["init", "req"]
|
|
141
|
+
"""
|
|
142
|
+
if not tokens:
|
|
143
|
+
return tokens
|
|
144
|
+
first = tokens[0]
|
|
145
|
+
if first not in alias_table:
|
|
146
|
+
return tokens
|
|
147
|
+
expansion = alias_table[first].split()
|
|
148
|
+
return expansion + tokens[1:]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def save_user_alias(name: str, target: str) -> Path:
|
|
152
|
+
"""Add or update *name → target* in the user aliases.toml.
|
|
153
|
+
|
|
154
|
+
Returns the path of the written file.
|
|
155
|
+
"""
|
|
156
|
+
path = _user_alias_path()
|
|
157
|
+
existing = _read_toml_aliases(path)
|
|
158
|
+
existing[name] = target
|
|
159
|
+
_write_toml_aliases(path, existing)
|
|
160
|
+
return path
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def remove_user_alias(name: str) -> Path:
|
|
164
|
+
"""Remove *name* from the user aliases.toml.
|
|
165
|
+
|
|
166
|
+
Returns the path of the written file.
|
|
167
|
+
|
|
168
|
+
Raises
|
|
169
|
+
------
|
|
170
|
+
KeyError
|
|
171
|
+
If *name* is not present.
|
|
172
|
+
"""
|
|
173
|
+
path = _user_alias_path()
|
|
174
|
+
existing = _read_toml_aliases(path)
|
|
175
|
+
if name not in existing:
|
|
176
|
+
raise KeyError(f"alias not found: {name!r}")
|
|
177
|
+
del existing[name]
|
|
178
|
+
_write_toml_aliases(path, existing)
|
|
179
|
+
return path
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def list_aliases(
|
|
183
|
+
*,
|
|
184
|
+
run_dir: str | Path | None = None,
|
|
185
|
+
extensions_default_aliases: Optional[list[dict[str, str]]] = None,
|
|
186
|
+
extension_names: Optional[list[str]] = None,
|
|
187
|
+
) -> dict[str, tuple[str, str]]:
|
|
188
|
+
"""Return ``{alias_name: (target, source)}`` with provenance.
|
|
189
|
+
|
|
190
|
+
*source* is one of:
|
|
191
|
+
|
|
192
|
+
- ``"run"`` — from ``<run_dir>/aliases.toml``
|
|
193
|
+
- ``"user"`` — from ``~/.config/arctx/aliases.toml``
|
|
194
|
+
- ``"ext:<name>"`` — from an extension's ``default_aliases()``
|
|
195
|
+
|
|
196
|
+
The same merge priority applies; this function exposes the winning source
|
|
197
|
+
for each alias name.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
extension_names:
|
|
202
|
+
Parallel list to *extensions_default_aliases* giving the extension name
|
|
203
|
+
for each entry. If None, sources are labelled ``"ext:0"``, ``"ext:1"``
|
|
204
|
+
etc.
|
|
205
|
+
"""
|
|
206
|
+
result: dict[str, tuple[str, str]] = {}
|
|
207
|
+
|
|
208
|
+
ext_aliases_list = extensions_default_aliases or []
|
|
209
|
+
ext_names_list = extension_names or []
|
|
210
|
+
|
|
211
|
+
# 1. Extension defaults (first ext wins)
|
|
212
|
+
for idx, ext_aliases in enumerate(ext_aliases_list):
|
|
213
|
+
ext_label = f"ext:{ext_names_list[idx]}" if idx < len(ext_names_list) else f"ext:{idx}"
|
|
214
|
+
for name, target in ext_aliases.items():
|
|
215
|
+
if name not in result:
|
|
216
|
+
result[name] = (target, ext_label)
|
|
217
|
+
|
|
218
|
+
# 2. User config
|
|
219
|
+
user_aliases = _read_toml_aliases(_user_alias_path())
|
|
220
|
+
for name, target in user_aliases.items():
|
|
221
|
+
result[name] = (target, "user")
|
|
222
|
+
|
|
223
|
+
# 3. Run-local (highest priority)
|
|
224
|
+
if run_dir is not None:
|
|
225
|
+
run_aliases = _read_toml_aliases(_run_alias_path(run_dir))
|
|
226
|
+
for name, target in run_aliases.items():
|
|
227
|
+
result[name] = (target, "run")
|
|
228
|
+
|
|
229
|
+
return result
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
__all__ = [
|
|
233
|
+
"load_alias_table",
|
|
234
|
+
"resolve_alias",
|
|
235
|
+
"save_user_alias",
|
|
236
|
+
"remove_user_alias",
|
|
237
|
+
"list_aliases",
|
|
238
|
+
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Helpers for turning in-memory mutations into SQLite append batches."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from arctx.core.append import AppendBatch, GraphRecordEnvelope
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def graph_counts(handle) -> dict[str, set[str]]:
|
|
9
|
+
"""Capture graph record IDs before a mutation."""
|
|
10
|
+
return {
|
|
11
|
+
"nodes": set(handle.run_graph.nodes),
|
|
12
|
+
"transitions": set(handle.run_graph.transitions),
|
|
13
|
+
"payloads": set(handle.run_graph.payloads),
|
|
14
|
+
"views": {view.view_id for view in handle.run_graph.views.values()},
|
|
15
|
+
"work_events": {event.event_id for event in handle.run_graph.work_events},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def maybe_append_or_save(
|
|
20
|
+
*,
|
|
21
|
+
store,
|
|
22
|
+
handle,
|
|
23
|
+
user_id: str | None,
|
|
24
|
+
work_session_id: str | None,
|
|
25
|
+
before: dict[str, set[str]],
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Use append_batch for capable stores, otherwise fall back to save_run."""
|
|
28
|
+
if user_id is None or work_session_id is None or not hasattr(store, "append_batch"):
|
|
29
|
+
store.save_run(handle)
|
|
30
|
+
return
|
|
31
|
+
store.append_batch(
|
|
32
|
+
build_append_batch(
|
|
33
|
+
handle,
|
|
34
|
+
user_id=user_id,
|
|
35
|
+
work_session_id=work_session_id,
|
|
36
|
+
before=before,
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_append_batch(
|
|
42
|
+
handle,
|
|
43
|
+
*,
|
|
44
|
+
user_id: str,
|
|
45
|
+
work_session_id: str,
|
|
46
|
+
before: dict[str, set[str]],
|
|
47
|
+
) -> AppendBatch:
|
|
48
|
+
"""Build an append batch from records added since *before*."""
|
|
49
|
+
records: list[GraphRecordEnvelope] = []
|
|
50
|
+
|
|
51
|
+
for node_id in _new_ids(handle.run_graph.nodes, before, "nodes"):
|
|
52
|
+
node = handle.run_graph.nodes[node_id]
|
|
53
|
+
records.append(GraphRecordEnvelope("node", node.node_id, node))
|
|
54
|
+
|
|
55
|
+
for transition_id in _new_ids(handle.run_graph.transitions, before, "transitions"):
|
|
56
|
+
transition = handle.run_graph.transitions[transition_id]
|
|
57
|
+
records.append(GraphRecordEnvelope("transition", transition.transition_id, transition))
|
|
58
|
+
|
|
59
|
+
for payload_id in _new_ids(handle.run_graph.payloads, before, "payloads"):
|
|
60
|
+
payload = handle.run_graph.payloads[payload_id]
|
|
61
|
+
records.append(GraphRecordEnvelope("payload", payload.payload_id, payload))
|
|
62
|
+
|
|
63
|
+
before_view_ids = before.get("views", set())
|
|
64
|
+
new_views = [
|
|
65
|
+
view for view in handle.run_graph.views.values() if view.view_id not in before_view_ids
|
|
66
|
+
]
|
|
67
|
+
for view in new_views:
|
|
68
|
+
records.append(GraphRecordEnvelope("view", view.view_id, view))
|
|
69
|
+
|
|
70
|
+
new_events = [
|
|
71
|
+
event
|
|
72
|
+
for event in handle.run_graph.work_events
|
|
73
|
+
if event.event_id not in before.get("work_events", set())
|
|
74
|
+
]
|
|
75
|
+
if not new_events:
|
|
76
|
+
raise RuntimeError("append batch requires at least one work event")
|
|
77
|
+
|
|
78
|
+
session = handle.run_graph.work_sessions[work_session_id]
|
|
79
|
+
return AppendBatch(
|
|
80
|
+
run_id=handle.run_id,
|
|
81
|
+
user_id=user_id,
|
|
82
|
+
work_session_id=work_session_id,
|
|
83
|
+
work_session=session,
|
|
84
|
+
events=tuple(new_events),
|
|
85
|
+
records=tuple(records),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _new_ids(current: dict[str, object], before: dict[str, set[str]], key: str) -> list[str]:
|
|
90
|
+
return [record_id for record_id in current if record_id not in before.get(key, set())]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Core CLI command registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from arctx.ext import CliCommand
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def core_cli_commands() -> list[CliCommand]:
|
|
9
|
+
"""Return the built-in, extension-independent CLI commands."""
|
|
10
|
+
from arctx_cli.commands.alias_cmd import add_parser as add_alias_parser
|
|
11
|
+
from arctx_cli.commands.alias_cmd import cli_alias
|
|
12
|
+
from arctx_cli.commands.anchor import add_parser as add_anchor_parser
|
|
13
|
+
from arctx_cli.commands.anchor import cli_anchor
|
|
14
|
+
from arctx_cli.commands.current import add_parser as add_current_parser
|
|
15
|
+
from arctx_cli.commands.current import cli_current
|
|
16
|
+
from arctx_cli.commands.cut import add_parser as add_cut_parser
|
|
17
|
+
from arctx_cli.commands.cut import cli_cut
|
|
18
|
+
from arctx_cli.commands.dump import add_parser as add_dump_parser
|
|
19
|
+
from arctx_cli.commands.dump import cli_dump
|
|
20
|
+
from arctx_cli.commands.ext import add_parser as add_ext_parser
|
|
21
|
+
from arctx_cli.commands.ext import cli_ext
|
|
22
|
+
from arctx_cli.commands.graph import add_parser as add_graph_parser
|
|
23
|
+
from arctx_cli.commands.graph import cli_graph
|
|
24
|
+
from arctx_cli.commands.guide import add_parser as add_guide_parser
|
|
25
|
+
from arctx_cli.commands.guide import cli_guide
|
|
26
|
+
from arctx_cli.commands.init import add_parser as add_init_parser
|
|
27
|
+
from arctx_cli.commands.init import cli_init
|
|
28
|
+
from arctx_cli.commands.list import add_parser as add_list_parser
|
|
29
|
+
from arctx_cli.commands.list import cli_list
|
|
30
|
+
from arctx_cli.commands.migrate import add_parser as add_migrate_parser
|
|
31
|
+
from arctx_cli.commands.migrate import cli_migrate
|
|
32
|
+
from arctx_cli.commands.node import add_parser as add_node_parser
|
|
33
|
+
from arctx_cli.commands.node import cli_node
|
|
34
|
+
from arctx_cli.commands.outcomes import add_parser as add_outcomes_parser
|
|
35
|
+
from arctx_cli.commands.outcomes import cli_outcomes
|
|
36
|
+
from arctx_cli.commands.payload import add_parser as add_payload_parser
|
|
37
|
+
from arctx_cli.commands.payload import cli_payload
|
|
38
|
+
from arctx_cli.commands.reachable import add_parser as add_reachable_parser
|
|
39
|
+
from arctx_cli.commands.reachable import cli_reachable
|
|
40
|
+
from arctx_cli.commands.show import add_parser as add_show_parser
|
|
41
|
+
from arctx_cli.commands.show import cli_show
|
|
42
|
+
from arctx_cli.commands.sync import add_parser as add_sync_parser
|
|
43
|
+
from arctx_cli.commands.sync import cli_sync
|
|
44
|
+
from arctx_cli.commands.trace import add_parser as add_trace_parser
|
|
45
|
+
from arctx_cli.commands.trace import cli_trace
|
|
46
|
+
from arctx_cli.commands.transition import add_parser as add_transition_parser
|
|
47
|
+
from arctx_cli.commands.transition import cli_transition
|
|
48
|
+
from arctx_cli.commands.use import add_parser as add_use_parser
|
|
49
|
+
from arctx_cli.commands.use import cli_use
|
|
50
|
+
from arctx_cli.commands.view import add_parser as add_view_parser
|
|
51
|
+
from arctx_cli.commands.view import cli_view
|
|
52
|
+
from arctx_cli.commands.work_session import add_parser as add_work_session_parser
|
|
53
|
+
from arctx_cli.commands.work_session import cli_work_session
|
|
54
|
+
|
|
55
|
+
return [
|
|
56
|
+
CliCommand("alias", add_alias_parser, cli_alias),
|
|
57
|
+
CliCommand("anchor", add_anchor_parser, cli_anchor),
|
|
58
|
+
CliCommand("current", add_current_parser, cli_current),
|
|
59
|
+
CliCommand("ext", add_ext_parser, cli_ext),
|
|
60
|
+
CliCommand("dump", add_dump_parser, cli_dump),
|
|
61
|
+
CliCommand("graph", add_graph_parser, cli_graph),
|
|
62
|
+
CliCommand("guide", add_guide_parser, cli_guide),
|
|
63
|
+
CliCommand("init", add_init_parser, cli_init),
|
|
64
|
+
CliCommand("list", add_list_parser, cli_list),
|
|
65
|
+
CliCommand("migrate", add_migrate_parser, cli_migrate),
|
|
66
|
+
CliCommand("node", add_node_parser, cli_node),
|
|
67
|
+
CliCommand("outcomes", add_outcomes_parser, cli_outcomes),
|
|
68
|
+
CliCommand("payload", add_payload_parser, cli_payload),
|
|
69
|
+
CliCommand("reachable", add_reachable_parser, cli_reachable),
|
|
70
|
+
CliCommand("cut", add_cut_parser, cli_cut),
|
|
71
|
+
CliCommand("show", add_show_parser, cli_show),
|
|
72
|
+
CliCommand("sync", add_sync_parser, cli_sync),
|
|
73
|
+
CliCommand("trace", add_trace_parser, cli_trace),
|
|
74
|
+
CliCommand("transition", add_transition_parser, cli_transition),
|
|
75
|
+
CliCommand("use", add_use_parser, cli_use),
|
|
76
|
+
CliCommand("view", add_view_parser, cli_view),
|
|
77
|
+
CliCommand("work-session", add_work_session_parser, cli_work_session),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def register_cli_commands(subparsers, commands: list[CliCommand]) -> None:
|
|
82
|
+
"""Register CLI commands and attach their dispatch handlers."""
|
|
83
|
+
for command in commands:
|
|
84
|
+
parser = command.add_parser(subparsers)
|
|
85
|
+
parser.set_defaults(_arctx_handler=command.handler)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""arctx alias subcommand — manage CLI aliases.
|
|
2
|
+
|
|
3
|
+
Subcommands
|
|
4
|
+
-----------
|
|
5
|
+
list Show the current alias table with provenance (run > user > ext).
|
|
6
|
+
add Add or update an alias in the user config.
|
|
7
|
+
remove Remove an alias from the user config.
|
|
8
|
+
resolve Show the expansion of a given alias token (debug helper).
|
|
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 ``alias`` subcommand."""
|
|
20
|
+
parser = subparsers.add_parser("alias", help="Manage CLI aliases")
|
|
21
|
+
alias_sub = parser.add_subparsers(dest="alias_command", required=True)
|
|
22
|
+
|
|
23
|
+
# alias list
|
|
24
|
+
al_list = alias_sub.add_parser("list", help="Show all aliases with provenance")
|
|
25
|
+
al_list.add_argument("--run", default=None, help="Run ID")
|
|
26
|
+
al_list.add_argument("--store-dir", default=None, dest="store_dir")
|
|
27
|
+
|
|
28
|
+
# alias add <name> <target>
|
|
29
|
+
al_add = alias_sub.add_parser("add", help="Add or update a user alias")
|
|
30
|
+
al_add.add_argument("name", help="Alias name (e.g. 'c')")
|
|
31
|
+
al_add.add_argument("target", help="Alias target (e.g. 'git commit')")
|
|
32
|
+
|
|
33
|
+
# alias remove <name>
|
|
34
|
+
al_remove = alias_sub.add_parser("remove", help="Remove a user alias")
|
|
35
|
+
al_remove.add_argument("name", help="Alias name to remove")
|
|
36
|
+
|
|
37
|
+
# alias resolve <name>
|
|
38
|
+
al_resolve = alias_sub.add_parser("resolve", help="Show how a token resolves")
|
|
39
|
+
al_resolve.add_argument("name", help="Token to resolve")
|
|
40
|
+
al_resolve.add_argument("--run", default=None, help="Run ID")
|
|
41
|
+
al_resolve.add_argument("--store-dir", default=None, dest="store_dir")
|
|
42
|
+
|
|
43
|
+
return parser
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Helpers
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _resolve_run_dir_from_args(args) -> str | None:
|
|
52
|
+
"""Best-effort: resolve run_dir from args."""
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
|
|
55
|
+
store_dir = getattr(args, "store_dir", None)
|
|
56
|
+
run_id_arg = getattr(args, "run", None)
|
|
57
|
+
if run_id_arg is None:
|
|
58
|
+
import os
|
|
59
|
+
|
|
60
|
+
run_id_arg = os.environ.get("ARCTX_RUN_ID")
|
|
61
|
+
if run_id_arg and store_dir:
|
|
62
|
+
return str(Path(store_dir) / run_id_arg)
|
|
63
|
+
if run_id_arg:
|
|
64
|
+
from arctx_cli.paths import resolve_store_dir
|
|
65
|
+
|
|
66
|
+
return str(Path(resolve_store_dir()) / run_id_arg)
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _collect_ext_aliases(run_dir: str | None) -> tuple[list[dict[str, str]], list[str]]:
|
|
71
|
+
"""Load default_aliases from standard and enabled extensions."""
|
|
72
|
+
from arctx.ext import load_extension
|
|
73
|
+
from arctx.ext.enabled import load_enabled
|
|
74
|
+
|
|
75
|
+
ext_aliases: list[dict[str, str]] = []
|
|
76
|
+
ext_names: list[str] = []
|
|
77
|
+
seen: set[str] = set()
|
|
78
|
+
for ext_name in ["git"]:
|
|
79
|
+
try:
|
|
80
|
+
ext = load_extension(ext_name)
|
|
81
|
+
ext_aliases.append(ext.default_aliases())
|
|
82
|
+
ext_names.append(ext.name)
|
|
83
|
+
seen.add(ext.name)
|
|
84
|
+
except (KeyError, ImportError):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
if run_dir is None:
|
|
88
|
+
return ext_aliases, ext_names
|
|
89
|
+
|
|
90
|
+
for ee in load_enabled(run_dir):
|
|
91
|
+
if ee.name in seen:
|
|
92
|
+
continue
|
|
93
|
+
try:
|
|
94
|
+
ext = load_extension(ee.name)
|
|
95
|
+
ext_aliases.append(ext.default_aliases())
|
|
96
|
+
ext_names.append(ext.name)
|
|
97
|
+
seen.add(ext.name)
|
|
98
|
+
except (KeyError, ImportError):
|
|
99
|
+
continue
|
|
100
|
+
return ext_aliases, ext_names
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# CLI dispatcher
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def cli_alias(args) -> int:
|
|
109
|
+
"""Dispatch arctx alias subcommands."""
|
|
110
|
+
from arctx_cli.alias import list_aliases, remove_user_alias, resolve_alias, save_user_alias
|
|
111
|
+
|
|
112
|
+
cmd = args.alias_command
|
|
113
|
+
|
|
114
|
+
if cmd == "list":
|
|
115
|
+
run_dir = _resolve_run_dir_from_args(args)
|
|
116
|
+
ext_aliases, ext_names = _collect_ext_aliases(run_dir)
|
|
117
|
+
table = list_aliases(
|
|
118
|
+
run_dir=run_dir,
|
|
119
|
+
extensions_default_aliases=ext_aliases,
|
|
120
|
+
extension_names=ext_names,
|
|
121
|
+
)
|
|
122
|
+
output = {
|
|
123
|
+
name: {"target": target, "source": source}
|
|
124
|
+
for name, (target, source) in table.items()
|
|
125
|
+
}
|
|
126
|
+
print(json.dumps(output, indent=2, ensure_ascii=False, sort_keys=True))
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
if cmd == "add":
|
|
130
|
+
path = save_user_alias(args.name, args.target)
|
|
131
|
+
print(f"alias {args.name!r} -> {args.target!r} saved to {path}", file=sys.stderr)
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
if cmd == "remove":
|
|
135
|
+
try:
|
|
136
|
+
path = remove_user_alias(args.name)
|
|
137
|
+
except KeyError as exc:
|
|
138
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
139
|
+
return 1
|
|
140
|
+
print(f"alias {args.name!r} removed from {path}", file=sys.stderr)
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
if cmd == "resolve":
|
|
144
|
+
run_dir = _resolve_run_dir_from_args(args)
|
|
145
|
+
ext_aliases, _ = _collect_ext_aliases(run_dir)
|
|
146
|
+
from arctx_cli.alias import load_alias_table
|
|
147
|
+
|
|
148
|
+
table = load_alias_table(run_dir=run_dir, extensions_default_aliases=ext_aliases)
|
|
149
|
+
expanded = resolve_alias(table, [args.name])
|
|
150
|
+
if expanded == [args.name]:
|
|
151
|
+
result = {"input": args.name, "resolved": False, "tokens": expanded}
|
|
152
|
+
else:
|
|
153
|
+
source = "unknown"
|
|
154
|
+
from arctx_cli.alias import list_aliases as _la
|
|
155
|
+
|
|
156
|
+
ext_aliases2, ext_names2 = _collect_ext_aliases(run_dir)
|
|
157
|
+
provenance = _la(
|
|
158
|
+
run_dir=run_dir,
|
|
159
|
+
extensions_default_aliases=ext_aliases2,
|
|
160
|
+
extension_names=ext_names2,
|
|
161
|
+
)
|
|
162
|
+
if args.name in provenance:
|
|
163
|
+
source = provenance[args.name][1]
|
|
164
|
+
result = {
|
|
165
|
+
"input": args.name,
|
|
166
|
+
"resolved": True,
|
|
167
|
+
"tokens": expanded,
|
|
168
|
+
"target": table[args.name],
|
|
169
|
+
"source": source,
|
|
170
|
+
}
|
|
171
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
172
|
+
return 0
|
|
173
|
+
|
|
174
|
+
return 1
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""arctx CLI anchor 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(
|
|
19
|
+
"anchor",
|
|
20
|
+
help="Create a scope anchor node from an existing node",
|
|
21
|
+
description=(
|
|
22
|
+
"Create a lightweight branching anchor using a scope_refinement plan "
|
|
23
|
+
"and a completed result."
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument("--run", default=None)
|
|
27
|
+
parser.add_argument("--from", required=True, dest="from_node_id", metavar="NODE_ID")
|
|
28
|
+
parser.add_argument("--label", required=True)
|
|
29
|
+
parser.add_argument("--store-dir", default=None)
|
|
30
|
+
parser.add_argument("--user", default=None)
|
|
31
|
+
parser.add_argument("--work-session", default=None)
|
|
32
|
+
return parser
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_anchor_command(
|
|
36
|
+
*,
|
|
37
|
+
run_id: str,
|
|
38
|
+
from_node_id: str,
|
|
39
|
+
label: str,
|
|
40
|
+
store_dir: str,
|
|
41
|
+
user_id: str | None = None,
|
|
42
|
+
work_session_id: str | None = None,
|
|
43
|
+
) -> dict:
|
|
44
|
+
store = resolve_store(store_dir)
|
|
45
|
+
if not store.run_path(run_id).exists():
|
|
46
|
+
raise KeyError(f"unknown run_id: {run_id}")
|
|
47
|
+
handle = store.load_run(run_id)
|
|
48
|
+
before = graph_counts(handle)
|
|
49
|
+
node = handle.anchor(
|
|
50
|
+
from_node_id,
|
|
51
|
+
label,
|
|
52
|
+
user_id=user_id,
|
|
53
|
+
work_session_id=work_session_id,
|
|
54
|
+
)
|
|
55
|
+
transition_id = handle.run_graph.transitions_to_node(node.node_id)[0]
|
|
56
|
+
maybe_append_or_save(
|
|
57
|
+
store=store,
|
|
58
|
+
handle=handle,
|
|
59
|
+
user_id=user_id,
|
|
60
|
+
work_session_id=work_session_id,
|
|
61
|
+
before=before,
|
|
62
|
+
)
|
|
63
|
+
return {
|
|
64
|
+
"anchor": {
|
|
65
|
+
"transition_id": transition_id,
|
|
66
|
+
"node_id": node.node_id,
|
|
67
|
+
"label": label,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def cli_anchor(args) -> int:
|
|
73
|
+
result = run_anchor_command(
|
|
74
|
+
run_id=resolve_run_id_from_args(args),
|
|
75
|
+
from_node_id=args.from_node_id,
|
|
76
|
+
label=args.label,
|
|
77
|
+
store_dir=args.store_dir,
|
|
78
|
+
user_id=resolve_user_id_from_args(args),
|
|
79
|
+
work_session_id=resolve_work_session_id_from_args(args),
|
|
80
|
+
)
|
|
81
|
+
print(json.dumps(result["anchor"], ensure_ascii=False, indent=2))
|
|
82
|
+
return 0
|