contree-cli 0.2.3__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.
- contree_cli/__init__.py +27 -0
- contree_cli/__main__.py +62 -0
- contree_cli/arguments.py +141 -0
- contree_cli/cli/__init__.py +0 -0
- contree_cli/cli/auth.py +124 -0
- contree_cli/cli/cat.py +74 -0
- contree_cli/cli/cd.py +49 -0
- contree_cli/cli/cp.py +107 -0
- contree_cli/cli/file.py +179 -0
- contree_cli/cli/images.py +88 -0
- contree_cli/cli/kill.py +86 -0
- contree_cli/cli/ls.py +83 -0
- contree_cli/cli/ps.py +120 -0
- contree_cli/cli/run.py +438 -0
- contree_cli/cli/session.py +282 -0
- contree_cli/cli/show.py +97 -0
- contree_cli/cli/tag.py +50 -0
- contree_cli/cli/use.py +119 -0
- contree_cli/client.py +222 -0
- contree_cli/config.py +116 -0
- contree_cli/log.py +40 -0
- contree_cli/mapped_file.py +112 -0
- contree_cli/output.py +376 -0
- contree_cli/session.py +761 -0
- contree_cli/shell/__init__.py +59 -0
- contree_cli/shell/completer.py +465 -0
- contree_cli/shell/history.py +53 -0
- contree_cli/shell/parser.py +107 -0
- contree_cli/shell/repl.py +486 -0
- contree_cli/shell/trie.py +113 -0
- contree_cli/types.py +87 -0
- contree_cli-0.2.3.dist-info/METADATA +323 -0
- contree_cli-0.2.3.dist-info/RECORD +35 -0
- contree_cli-0.2.3.dist-info/WHEEL +4 -0
- contree_cli-0.2.3.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Manage session branches and history.
|
|
2
|
+
|
|
3
|
+
Without a subcommand, shows the current session info (key, branch,
|
|
4
|
+
image, last operation).
|
|
5
|
+
|
|
6
|
+
Subcommands:
|
|
7
|
+
list (ls) List all sessions
|
|
8
|
+
use KEY Import another session's current image
|
|
9
|
+
branch (br) List or create branches (--from to fork)
|
|
10
|
+
checkout (co) Switch active branch
|
|
11
|
+
rollback (rb) Roll back N steps in history
|
|
12
|
+
show Display the session history DAG
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import sys
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
|
|
20
|
+
from contree_cli import FORMATTER, SESSION_STORE, ArgumentsProtocol, SetupResult
|
|
21
|
+
from contree_cli.output import DefaultFormatter
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class SessionInfoArgs(ArgumentsProtocol):
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_args(cls, ns: argparse.Namespace) -> SessionInfoArgs:
|
|
28
|
+
return cls()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class ListArgs(ArgumentsProtocol):
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_args(cls, ns: argparse.Namespace) -> ListArgs:
|
|
35
|
+
return cls()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class UseSessionArgs(ArgumentsProtocol):
|
|
40
|
+
name: str
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_args(cls, ns: argparse.Namespace) -> UseSessionArgs:
|
|
44
|
+
return cls(name=ns.session_name)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class BranchArgs(ArgumentsProtocol):
|
|
49
|
+
name: str | None
|
|
50
|
+
from_branch: str | None
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_args(cls, ns: argparse.Namespace) -> BranchArgs:
|
|
54
|
+
return cls(
|
|
55
|
+
name=getattr(ns, "branch_name", None),
|
|
56
|
+
from_branch=getattr(ns, "from_branch", None),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class RollbackArgs(ArgumentsProtocol):
|
|
62
|
+
n: int
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_args(cls, ns: argparse.Namespace) -> RollbackArgs:
|
|
66
|
+
return cls(n=ns.n)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class CheckoutArgs(ArgumentsProtocol):
|
|
71
|
+
name: str
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_args(cls, ns: argparse.Namespace) -> CheckoutArgs:
|
|
75
|
+
return cls(name=ns.checkout_branch)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class ShowArgs(ArgumentsProtocol):
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_args(cls, ns: argparse.Namespace) -> ShowArgs:
|
|
82
|
+
return cls()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def setup_parser(p: argparse.ArgumentParser) -> SetupResult:
|
|
86
|
+
sub = p.add_subparsers(dest="session_action")
|
|
87
|
+
|
|
88
|
+
# session list
|
|
89
|
+
list_p = sub.add_parser(
|
|
90
|
+
"list", aliases=["ls"],
|
|
91
|
+
help="List all sessions",
|
|
92
|
+
)
|
|
93
|
+
list_p.set_defaults(handler=cmd_list, load_args=ListArgs)
|
|
94
|
+
|
|
95
|
+
# session use SESSION
|
|
96
|
+
use_p = sub.add_parser("use", help="Import another session's image")
|
|
97
|
+
use_p.add_argument("session_name", help="Session key or suffix to match")
|
|
98
|
+
use_p.set_defaults(handler=cmd_use_session, load_args=UseSessionArgs)
|
|
99
|
+
|
|
100
|
+
# session branch [NAME] [--from BRANCH]
|
|
101
|
+
branch_p = sub.add_parser(
|
|
102
|
+
"branch", aliases=["br"],
|
|
103
|
+
help="List or create branches",
|
|
104
|
+
)
|
|
105
|
+
branch_p.add_argument(
|
|
106
|
+
"branch_name", nargs="?", default=None,
|
|
107
|
+
help="New branch name",
|
|
108
|
+
)
|
|
109
|
+
branch_p.add_argument(
|
|
110
|
+
"--from", dest="from_branch", default=None,
|
|
111
|
+
help="Source branch (default: active branch)",
|
|
112
|
+
)
|
|
113
|
+
branch_p.set_defaults(handler=cmd_branch, load_args=BranchArgs)
|
|
114
|
+
|
|
115
|
+
# session checkout BRANCH
|
|
116
|
+
co_p = sub.add_parser(
|
|
117
|
+
"checkout", aliases=["co"],
|
|
118
|
+
help="Switch active branch",
|
|
119
|
+
)
|
|
120
|
+
co_p.add_argument("checkout_branch", help="Branch to switch to")
|
|
121
|
+
co_p.set_defaults(
|
|
122
|
+
handler=cmd_checkout, load_args=CheckoutArgs,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# session rollback [N]
|
|
126
|
+
rollback_p = sub.add_parser(
|
|
127
|
+
"rollback", aliases=["rb"],
|
|
128
|
+
help="Roll back N steps",
|
|
129
|
+
)
|
|
130
|
+
rollback_p.add_argument(
|
|
131
|
+
"n", nargs="?", type=int, default=1,
|
|
132
|
+
help="Steps to rollback",
|
|
133
|
+
)
|
|
134
|
+
rollback_p.set_defaults(handler=cmd_rollback, load_args=RollbackArgs)
|
|
135
|
+
|
|
136
|
+
# session show
|
|
137
|
+
show_p = sub.add_parser("show", help="Show session history")
|
|
138
|
+
show_p.set_defaults(handler=cmd_show, load_args=ShowArgs)
|
|
139
|
+
|
|
140
|
+
return cmd_session_info, SessionInfoArgs
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def cmd_session_info(_: SessionInfoArgs) -> int | None:
|
|
144
|
+
store = SESSION_STORE.get()
|
|
145
|
+
session = store.session
|
|
146
|
+
if session is None:
|
|
147
|
+
print(
|
|
148
|
+
"No active session. Run `contree use IMAGE` to start one.",
|
|
149
|
+
file=sys.stderr,
|
|
150
|
+
)
|
|
151
|
+
return 1
|
|
152
|
+
formatter = FORMATTER.get()
|
|
153
|
+
formatter(
|
|
154
|
+
session_key=session.session_key,
|
|
155
|
+
active_branch=session.active_branch,
|
|
156
|
+
current_image=session.current_image,
|
|
157
|
+
last_kind=session.last_kind,
|
|
158
|
+
last_title=session.last_title,
|
|
159
|
+
updated_at=session.updated_at,
|
|
160
|
+
)
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def cmd_list(_: ListArgs) -> int | None:
|
|
165
|
+
store = SESSION_STORE.get()
|
|
166
|
+
sessions = store.list_sessions()
|
|
167
|
+
if not sessions:
|
|
168
|
+
print("No sessions found.", file=sys.stderr)
|
|
169
|
+
return None
|
|
170
|
+
formatter = FORMATTER.get()
|
|
171
|
+
current_key = store.session_key
|
|
172
|
+
for s in sessions:
|
|
173
|
+
active = s.session_key == current_key
|
|
174
|
+
formatter(
|
|
175
|
+
active=active,
|
|
176
|
+
session_key=s.session_key,
|
|
177
|
+
active_branch=s.active_branch,
|
|
178
|
+
current_image=s.current_image,
|
|
179
|
+
updated_at=s.updated_at,
|
|
180
|
+
)
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def cmd_use_session(args: UseSessionArgs) -> int | None:
|
|
185
|
+
store = SESSION_STORE.get()
|
|
186
|
+
try:
|
|
187
|
+
target = store.find_session(args.name)
|
|
188
|
+
except ValueError as exc:
|
|
189
|
+
print(str(exc), file=sys.stderr)
|
|
190
|
+
return 1
|
|
191
|
+
store.set_image(
|
|
192
|
+
target.current_image,
|
|
193
|
+
kind="session use",
|
|
194
|
+
title=target.session_key,
|
|
195
|
+
)
|
|
196
|
+
formatter = FORMATTER.get()
|
|
197
|
+
formatter(
|
|
198
|
+
image=target.current_image,
|
|
199
|
+
from_session=target.session_key,
|
|
200
|
+
)
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def cmd_branch(args: BranchArgs) -> int | None:
|
|
205
|
+
store = SESSION_STORE.get()
|
|
206
|
+
if args.name is None:
|
|
207
|
+
# List branches
|
|
208
|
+
branches = store.list_branches()
|
|
209
|
+
if not branches:
|
|
210
|
+
print("No branches (no active session).", file=sys.stderr)
|
|
211
|
+
return None
|
|
212
|
+
formatter = FORMATTER.get()
|
|
213
|
+
for name, is_active in branches:
|
|
214
|
+
if isinstance(formatter, DefaultFormatter):
|
|
215
|
+
marker = "* " if is_active else " "
|
|
216
|
+
formatter(marker=marker, branch=name)
|
|
217
|
+
else:
|
|
218
|
+
formatter(branch=name, active=is_active)
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
# Create branch
|
|
222
|
+
try:
|
|
223
|
+
store.create_branch(args.name, args.from_branch)
|
|
224
|
+
except ValueError as exc:
|
|
225
|
+
print(str(exc), file=sys.stderr)
|
|
226
|
+
return 1
|
|
227
|
+
print(f"Created branch {args.name!r}")
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def cmd_checkout(args: CheckoutArgs) -> int | None:
|
|
232
|
+
store = SESSION_STORE.get()
|
|
233
|
+
try:
|
|
234
|
+
entry = store.switch_branch(args.name)
|
|
235
|
+
except ValueError as exc:
|
|
236
|
+
print(str(exc), file=sys.stderr)
|
|
237
|
+
return 1
|
|
238
|
+
formatter = FORMATTER.get()
|
|
239
|
+
formatter(
|
|
240
|
+
branch=args.name,
|
|
241
|
+
image=entry.image_uuid,
|
|
242
|
+
)
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def cmd_rollback(args: RollbackArgs) -> int | None:
|
|
247
|
+
store = SESSION_STORE.get()
|
|
248
|
+
try:
|
|
249
|
+
entry = store.rollback(args.n)
|
|
250
|
+
except ValueError as exc:
|
|
251
|
+
print(str(exc), file=sys.stderr)
|
|
252
|
+
return 1
|
|
253
|
+
formatter = FORMATTER.get()
|
|
254
|
+
formatter(
|
|
255
|
+
image=entry.image_uuid,
|
|
256
|
+
kind=entry.kind,
|
|
257
|
+
title=entry.title,
|
|
258
|
+
created_at=entry.created_at,
|
|
259
|
+
)
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def cmd_show(_: ShowArgs) -> int | None:
|
|
264
|
+
store = SESSION_STORE.get()
|
|
265
|
+
entries, branch_map = store.history_dag()
|
|
266
|
+
if not entries:
|
|
267
|
+
print("No history.", file=sys.stderr)
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
formatter = FORMATTER.get()
|
|
271
|
+
for entry in entries:
|
|
272
|
+
branches = branch_map.get(entry.id, [])
|
|
273
|
+
formatter(
|
|
274
|
+
id=entry.id,
|
|
275
|
+
image=entry.image_uuid,
|
|
276
|
+
parent_id=entry.parent_id,
|
|
277
|
+
kind=entry.kind,
|
|
278
|
+
title=entry.title,
|
|
279
|
+
created_at=entry.created_at,
|
|
280
|
+
branches=", ".join(branches) if branches else "",
|
|
281
|
+
)
|
|
282
|
+
return None
|
contree_cli/cli/show.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Show the result of an operation.
|
|
2
|
+
|
|
3
|
+
Fetches the operation by UUID and displays its status, duration, exit
|
|
4
|
+
code, result image, and captured stdout/stderr. Terminal operations
|
|
5
|
+
(SUCCESS, FAILED, CANCELLED) are cached locally to avoid redundant API
|
|
6
|
+
calls.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import sys
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import timedelta
|
|
16
|
+
from typing import Any, cast
|
|
17
|
+
|
|
18
|
+
from contree_cli import CLIENT, FORMATTER, SESSION_STORE, ArgumentsProtocol, SetupResult
|
|
19
|
+
from contree_cli.client import decode_stream
|
|
20
|
+
from contree_cli.output import DefaultFormatter, JSONFormatter, JSONPrettyFormatter
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class ShowArgs(ArgumentsProtocol):
|
|
27
|
+
uuid: str
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_args(cls, ns: argparse.Namespace) -> ShowArgs:
|
|
31
|
+
return cls(uuid=ns.uuid)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def setup_parser(p: argparse.ArgumentParser) -> SetupResult:
|
|
35
|
+
p.add_argument("uuid", help="Operation UUID")
|
|
36
|
+
return cmd_show, ShowArgs
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_TERMINAL = frozenset({"SUCCESS", "FAILED", "CANCELLED"})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def cmd_show(args: ShowArgs) -> None:
|
|
43
|
+
client = CLIENT.get()
|
|
44
|
+
formatter = FORMATTER.get()
|
|
45
|
+
store = SESSION_STORE.get()
|
|
46
|
+
|
|
47
|
+
cache_key = (args.uuid, "operation")
|
|
48
|
+
cached = store.cache.get(cache_key)
|
|
49
|
+
if cached is not None:
|
|
50
|
+
op = cast(dict[str, Any], cached)
|
|
51
|
+
else:
|
|
52
|
+
resp = client.get(f"/v1/operations/{args.uuid}")
|
|
53
|
+
op = json.loads(resp.read())
|
|
54
|
+
if op.get("status") in _TERMINAL:
|
|
55
|
+
store.cache[cache_key] = op
|
|
56
|
+
|
|
57
|
+
duration = (
|
|
58
|
+
timedelta(seconds=op["duration"])
|
|
59
|
+
if op.get("duration") is not None
|
|
60
|
+
else None
|
|
61
|
+
)
|
|
62
|
+
result = op.get("result") or {}
|
|
63
|
+
metadata = op.get("metadata") or {}
|
|
64
|
+
instance_result = metadata.get("result") or {}
|
|
65
|
+
|
|
66
|
+
exit_code = None
|
|
67
|
+
state = instance_result.get("state") or {}
|
|
68
|
+
if state:
|
|
69
|
+
exit_code = state.get("exit_code")
|
|
70
|
+
|
|
71
|
+
formatter(
|
|
72
|
+
uuid=op["uuid"],
|
|
73
|
+
kind=op["kind"],
|
|
74
|
+
status=op["status"],
|
|
75
|
+
duration=duration,
|
|
76
|
+
exit_code=exit_code,
|
|
77
|
+
error=op.get("error") or "",
|
|
78
|
+
image=result.get("image") or "",
|
|
79
|
+
tag=result.get("tag") or "",
|
|
80
|
+
)
|
|
81
|
+
formatter.flush()
|
|
82
|
+
|
|
83
|
+
_STREAM_FMTS = (DefaultFormatter, JSONFormatter, JSONPrettyFormatter)
|
|
84
|
+
if not isinstance(formatter, _STREAM_FMTS):
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
stdout = decode_stream(instance_result.get("stdout"))
|
|
88
|
+
stderr = decode_stream(instance_result.get("stderr"))
|
|
89
|
+
|
|
90
|
+
if stdout:
|
|
91
|
+
sys.stdout.write(stdout)
|
|
92
|
+
if not stdout.endswith("\n"):
|
|
93
|
+
sys.stdout.write("\n")
|
|
94
|
+
if stderr:
|
|
95
|
+
sys.stderr.write(stderr)
|
|
96
|
+
if not stderr.endswith("\n"):
|
|
97
|
+
sys.stderr.write("\n")
|
contree_cli/cli/tag.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Assign or remove a tag from an image.
|
|
2
|
+
|
|
3
|
+
Tags provide human-readable names for image UUIDs, making them easier
|
|
4
|
+
to reference in commands like `contree use tag:NAME`.
|
|
5
|
+
|
|
6
|
+
Use -d/--delete to remove a tag instead of assigning one.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import logging
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from contree_cli import CLIENT, ArgumentsProtocol, SetupResult
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class TagArgs(ArgumentsProtocol):
|
|
21
|
+
uuid: str
|
|
22
|
+
tag: str | None = None
|
|
23
|
+
delete: bool = False
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_args(cls, ns: argparse.Namespace) -> TagArgs:
|
|
27
|
+
return cls(uuid=ns.uuid, tag=ns.tag, delete=ns.delete)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def setup_parser(p: argparse.ArgumentParser) -> SetupResult:
|
|
31
|
+
p.add_argument("uuid", help="Image UUID")
|
|
32
|
+
p.add_argument("tag", help="Tag to assign")
|
|
33
|
+
p.add_argument(
|
|
34
|
+
"-d", "--delete", action="store_true",
|
|
35
|
+
help="Remove tag from image",
|
|
36
|
+
)
|
|
37
|
+
return cmd_tag, TagArgs
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def cmd_tag(args: TagArgs) -> int | None:
|
|
41
|
+
client = CLIENT.get()
|
|
42
|
+
|
|
43
|
+
if args.delete:
|
|
44
|
+
client.delete(f"/v1/images/{args.uuid}/tag?tag={args.tag}")
|
|
45
|
+
logger.info("Removed tag from image %s", args.uuid)
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
client.patch_json(f"/v1/images/{args.uuid}/tag", {"tag": args.tag})
|
|
49
|
+
logger.info("Tagged image %s as %s", args.uuid, args.tag)
|
|
50
|
+
return None
|
contree_cli/cli/use.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Set or show the current session image.
|
|
2
|
+
|
|
3
|
+
With an IMAGE argument, resolves it (UUID or tag:NAME) and sets it as
|
|
4
|
+
the active session image. Prints a shell export line so that the
|
|
5
|
+
session key can be captured with eval:
|
|
6
|
+
|
|
7
|
+
eval $(contree use tag:ubuntu:latest)
|
|
8
|
+
|
|
9
|
+
Without arguments, displays the current session info (image, branch,
|
|
10
|
+
last operation).
|
|
11
|
+
|
|
12
|
+
Use -N/--new to start a fresh session instead of resuming the current
|
|
13
|
+
one. The new session key is printed as an export line.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
import uuid
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import PurePosixPath
|
|
24
|
+
|
|
25
|
+
from contree_cli import (
|
|
26
|
+
CLIENT,
|
|
27
|
+
FORMATTER,
|
|
28
|
+
IN_SHELL,
|
|
29
|
+
SESSION_STORE,
|
|
30
|
+
ArgumentsProtocol,
|
|
31
|
+
SetupResult,
|
|
32
|
+
)
|
|
33
|
+
from contree_cli.client import resolve_image
|
|
34
|
+
from contree_cli.session import SessionStore, get_db_path
|
|
35
|
+
|
|
36
|
+
log = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _shell_name() -> str:
|
|
40
|
+
return PurePosixPath(os.getenv("SHELL", "/bin/sh")).name
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _print_shell_export(key: str, value: str) -> None:
|
|
44
|
+
if _shell_name() == "fish":
|
|
45
|
+
sys.stdout.write(f"set -gx {key} {value}\n")
|
|
46
|
+
else:
|
|
47
|
+
sys.stdout.write(f"export {key}={value}\n")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class UseArgs(ArgumentsProtocol):
|
|
52
|
+
image: str | None
|
|
53
|
+
new: bool
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_args(cls, ns: argparse.Namespace) -> UseArgs:
|
|
57
|
+
return cls(image=ns.image, new=ns.new)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def setup_parser(p: argparse.ArgumentParser) -> SetupResult:
|
|
61
|
+
p.add_argument("image", nargs="?", default=None, help="Image UUID or tag")
|
|
62
|
+
p.add_argument(
|
|
63
|
+
"--new", "-N", action="store_true", default=False,
|
|
64
|
+
help="Start a new session instead of resuming the current one",
|
|
65
|
+
)
|
|
66
|
+
return cmd_use, UseArgs
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def cmd_use(args: UseArgs) -> int | None:
|
|
70
|
+
store = SESSION_STORE.get()
|
|
71
|
+
|
|
72
|
+
if args.new:
|
|
73
|
+
if args.image is None:
|
|
74
|
+
print(
|
|
75
|
+
"--new requires an IMAGE argument.",
|
|
76
|
+
file=sys.stderr,
|
|
77
|
+
)
|
|
78
|
+
return 1
|
|
79
|
+
new_key = str(uuid.uuid4())
|
|
80
|
+
store = SessionStore(get_db_path(), new_key)
|
|
81
|
+
SESSION_STORE.set(store)
|
|
82
|
+
|
|
83
|
+
if args.image is not None:
|
|
84
|
+
client = CLIENT.get()
|
|
85
|
+
image_uuid = resolve_image(client, args.image)
|
|
86
|
+
store.set_image(image_uuid, kind="use", title=args.image)
|
|
87
|
+
if not IN_SHELL.get(False):
|
|
88
|
+
_print_shell_export("CONTREE_SESSION", store.session_key)
|
|
89
|
+
if args.new and sys.stdout.isatty():
|
|
90
|
+
if _shell_name() == "fish":
|
|
91
|
+
eval_hint = "eval (contree use -N IMAGE)"
|
|
92
|
+
else:
|
|
93
|
+
eval_hint = "eval $(contree use -N IMAGE)"
|
|
94
|
+
log.warning(
|
|
95
|
+
"Session is not active until exported. "
|
|
96
|
+
"Either paste the line above, or use: %s",
|
|
97
|
+
eval_hint,
|
|
98
|
+
)
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
session = store.session
|
|
102
|
+
if session is None:
|
|
103
|
+
print(
|
|
104
|
+
"No active session. Run `contree use IMAGE` to start one.",
|
|
105
|
+
file=sys.stderr,
|
|
106
|
+
)
|
|
107
|
+
return 1
|
|
108
|
+
|
|
109
|
+
formatter = FORMATTER.get()
|
|
110
|
+
|
|
111
|
+
formatter(
|
|
112
|
+
session_key=session.session_key,
|
|
113
|
+
active_branch=session.active_branch,
|
|
114
|
+
current_image=session.current_image,
|
|
115
|
+
last_kind=session.last_kind,
|
|
116
|
+
last_title=session.last_title,
|
|
117
|
+
updated_at=session.updated_at,
|
|
118
|
+
)
|
|
119
|
+
return None
|