kstlib 0.0.1a0__py3-none-any.whl → 1.0.0__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 (166) hide show
  1. kstlib/__init__.py +266 -1
  2. kstlib/__main__.py +16 -0
  3. kstlib/alerts/__init__.py +110 -0
  4. kstlib/alerts/channels/__init__.py +36 -0
  5. kstlib/alerts/channels/base.py +197 -0
  6. kstlib/alerts/channels/email.py +227 -0
  7. kstlib/alerts/channels/slack.py +389 -0
  8. kstlib/alerts/exceptions.py +72 -0
  9. kstlib/alerts/manager.py +651 -0
  10. kstlib/alerts/models.py +142 -0
  11. kstlib/alerts/throttle.py +263 -0
  12. kstlib/auth/__init__.py +139 -0
  13. kstlib/auth/callback.py +399 -0
  14. kstlib/auth/config.py +502 -0
  15. kstlib/auth/errors.py +127 -0
  16. kstlib/auth/models.py +316 -0
  17. kstlib/auth/providers/__init__.py +14 -0
  18. kstlib/auth/providers/base.py +393 -0
  19. kstlib/auth/providers/oauth2.py +645 -0
  20. kstlib/auth/providers/oidc.py +821 -0
  21. kstlib/auth/session.py +338 -0
  22. kstlib/auth/token.py +482 -0
  23. kstlib/cache/__init__.py +50 -0
  24. kstlib/cache/decorator.py +261 -0
  25. kstlib/cache/strategies.py +516 -0
  26. kstlib/cli/__init__.py +8 -0
  27. kstlib/cli/app.py +195 -0
  28. kstlib/cli/commands/__init__.py +5 -0
  29. kstlib/cli/commands/auth/__init__.py +39 -0
  30. kstlib/cli/commands/auth/common.py +122 -0
  31. kstlib/cli/commands/auth/login.py +325 -0
  32. kstlib/cli/commands/auth/logout.py +74 -0
  33. kstlib/cli/commands/auth/providers.py +57 -0
  34. kstlib/cli/commands/auth/status.py +291 -0
  35. kstlib/cli/commands/auth/token.py +199 -0
  36. kstlib/cli/commands/auth/whoami.py +106 -0
  37. kstlib/cli/commands/config.py +89 -0
  38. kstlib/cli/commands/ops/__init__.py +39 -0
  39. kstlib/cli/commands/ops/attach.py +49 -0
  40. kstlib/cli/commands/ops/common.py +269 -0
  41. kstlib/cli/commands/ops/list_sessions.py +252 -0
  42. kstlib/cli/commands/ops/logs.py +49 -0
  43. kstlib/cli/commands/ops/start.py +98 -0
  44. kstlib/cli/commands/ops/status.py +138 -0
  45. kstlib/cli/commands/ops/stop.py +60 -0
  46. kstlib/cli/commands/rapi/__init__.py +60 -0
  47. kstlib/cli/commands/rapi/call.py +341 -0
  48. kstlib/cli/commands/rapi/list.py +99 -0
  49. kstlib/cli/commands/rapi/show.py +206 -0
  50. kstlib/cli/commands/secrets/__init__.py +35 -0
  51. kstlib/cli/commands/secrets/common.py +425 -0
  52. kstlib/cli/commands/secrets/decrypt.py +88 -0
  53. kstlib/cli/commands/secrets/doctor.py +743 -0
  54. kstlib/cli/commands/secrets/encrypt.py +242 -0
  55. kstlib/cli/commands/secrets/shred.py +96 -0
  56. kstlib/cli/common.py +86 -0
  57. kstlib/config/__init__.py +76 -0
  58. kstlib/config/exceptions.py +110 -0
  59. kstlib/config/export.py +225 -0
  60. kstlib/config/loader.py +963 -0
  61. kstlib/config/sops.py +287 -0
  62. kstlib/db/__init__.py +54 -0
  63. kstlib/db/aiosqlcipher.py +137 -0
  64. kstlib/db/cipher.py +112 -0
  65. kstlib/db/database.py +367 -0
  66. kstlib/db/exceptions.py +25 -0
  67. kstlib/db/pool.py +302 -0
  68. kstlib/helpers/__init__.py +35 -0
  69. kstlib/helpers/exceptions.py +11 -0
  70. kstlib/helpers/time_trigger.py +396 -0
  71. kstlib/kstlib.conf.yml +890 -0
  72. kstlib/limits.py +963 -0
  73. kstlib/logging/__init__.py +108 -0
  74. kstlib/logging/manager.py +633 -0
  75. kstlib/mail/__init__.py +42 -0
  76. kstlib/mail/builder.py +626 -0
  77. kstlib/mail/exceptions.py +27 -0
  78. kstlib/mail/filesystem.py +248 -0
  79. kstlib/mail/transport.py +224 -0
  80. kstlib/mail/transports/__init__.py +19 -0
  81. kstlib/mail/transports/gmail.py +268 -0
  82. kstlib/mail/transports/resend.py +324 -0
  83. kstlib/mail/transports/smtp.py +326 -0
  84. kstlib/meta.py +72 -0
  85. kstlib/metrics/__init__.py +88 -0
  86. kstlib/metrics/decorators.py +1090 -0
  87. kstlib/metrics/exceptions.py +14 -0
  88. kstlib/monitoring/__init__.py +116 -0
  89. kstlib/monitoring/_styles.py +163 -0
  90. kstlib/monitoring/cell.py +57 -0
  91. kstlib/monitoring/config.py +424 -0
  92. kstlib/monitoring/delivery.py +579 -0
  93. kstlib/monitoring/exceptions.py +63 -0
  94. kstlib/monitoring/image.py +220 -0
  95. kstlib/monitoring/kv.py +79 -0
  96. kstlib/monitoring/list.py +69 -0
  97. kstlib/monitoring/metric.py +88 -0
  98. kstlib/monitoring/monitoring.py +341 -0
  99. kstlib/monitoring/renderer.py +139 -0
  100. kstlib/monitoring/service.py +392 -0
  101. kstlib/monitoring/table.py +129 -0
  102. kstlib/monitoring/types.py +56 -0
  103. kstlib/ops/__init__.py +86 -0
  104. kstlib/ops/base.py +148 -0
  105. kstlib/ops/container.py +577 -0
  106. kstlib/ops/exceptions.py +209 -0
  107. kstlib/ops/manager.py +407 -0
  108. kstlib/ops/models.py +176 -0
  109. kstlib/ops/tmux.py +372 -0
  110. kstlib/ops/validators.py +287 -0
  111. kstlib/py.typed +0 -0
  112. kstlib/rapi/__init__.py +118 -0
  113. kstlib/rapi/client.py +875 -0
  114. kstlib/rapi/config.py +861 -0
  115. kstlib/rapi/credentials.py +887 -0
  116. kstlib/rapi/exceptions.py +213 -0
  117. kstlib/resilience/__init__.py +101 -0
  118. kstlib/resilience/circuit_breaker.py +440 -0
  119. kstlib/resilience/exceptions.py +95 -0
  120. kstlib/resilience/heartbeat.py +491 -0
  121. kstlib/resilience/rate_limiter.py +506 -0
  122. kstlib/resilience/shutdown.py +417 -0
  123. kstlib/resilience/watchdog.py +637 -0
  124. kstlib/secrets/__init__.py +29 -0
  125. kstlib/secrets/exceptions.py +19 -0
  126. kstlib/secrets/models.py +62 -0
  127. kstlib/secrets/providers/__init__.py +79 -0
  128. kstlib/secrets/providers/base.py +58 -0
  129. kstlib/secrets/providers/environment.py +66 -0
  130. kstlib/secrets/providers/keyring.py +107 -0
  131. kstlib/secrets/providers/kms.py +223 -0
  132. kstlib/secrets/providers/kwargs.py +101 -0
  133. kstlib/secrets/providers/sops.py +209 -0
  134. kstlib/secrets/resolver.py +221 -0
  135. kstlib/secrets/sensitive.py +130 -0
  136. kstlib/secure/__init__.py +23 -0
  137. kstlib/secure/fs.py +194 -0
  138. kstlib/secure/permissions.py +70 -0
  139. kstlib/ssl.py +347 -0
  140. kstlib/ui/__init__.py +23 -0
  141. kstlib/ui/exceptions.py +26 -0
  142. kstlib/ui/panels.py +484 -0
  143. kstlib/ui/spinner.py +864 -0
  144. kstlib/ui/tables.py +382 -0
  145. kstlib/utils/__init__.py +48 -0
  146. kstlib/utils/dict.py +36 -0
  147. kstlib/utils/formatting.py +338 -0
  148. kstlib/utils/http_trace.py +237 -0
  149. kstlib/utils/lazy.py +49 -0
  150. kstlib/utils/secure_delete.py +205 -0
  151. kstlib/utils/serialization.py +247 -0
  152. kstlib/utils/text.py +56 -0
  153. kstlib/utils/validators.py +124 -0
  154. kstlib/websocket/__init__.py +97 -0
  155. kstlib/websocket/exceptions.py +214 -0
  156. kstlib/websocket/manager.py +1102 -0
  157. kstlib/websocket/models.py +361 -0
  158. kstlib-1.0.0.dist-info/METADATA +201 -0
  159. kstlib-1.0.0.dist-info/RECORD +163 -0
  160. {kstlib-0.0.1a0.dist-info → kstlib-1.0.0.dist-info}/WHEEL +1 -1
  161. kstlib-1.0.0.dist-info/entry_points.txt +2 -0
  162. kstlib-1.0.0.dist-info/licenses/LICENSE.md +9 -0
  163. kstlib-0.0.1a0.dist-info/METADATA +0 -29
  164. kstlib-0.0.1a0.dist-info/RECORD +0 -6
  165. kstlib-0.0.1a0.dist-info/licenses/LICENSE.md +0 -5
  166. {kstlib-0.0.1a0.dist-info → kstlib-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,106 @@
1
+ """Show user info from OIDC userinfo endpoint."""
2
+
3
+ # pylint: disable=too-many-locals,too-many-branches
4
+ # Justification: OIDC userinfo renderer with 3 output modes (raw/quiet/verbose) and
5
+ # claim mapping for 12 standard OIDC claims. Variables are mostly display labels and
6
+ # formatted values - extracting helpers would fragment the linear rendering logic.
7
+
8
+ from __future__ import annotations
9
+
10
+ import typer
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+
14
+ from kstlib.cli.common import console, exit_error
15
+ from kstlib.utils.formatting import format_timestamp
16
+ from kstlib.utils.serialization import to_json
17
+
18
+ from .common import PROVIDER_ARGUMENT, QUIET_OPTION, get_provider, resolve_provider_name
19
+
20
+
21
+ def whoami(
22
+ provider: str | None = PROVIDER_ARGUMENT,
23
+ quiet: bool = QUIET_OPTION,
24
+ raw: bool = typer.Option(
25
+ False,
26
+ "--raw",
27
+ help="Output raw JSON response.",
28
+ ),
29
+ ) -> None:
30
+ """Show user info from the OIDC userinfo endpoint.
31
+
32
+ Only works with OIDC providers that expose a userinfo endpoint.
33
+ """
34
+ provider_name = resolve_provider_name(provider)
35
+ auth_provider = get_provider(provider_name)
36
+
37
+ # Check if authenticated
38
+ token = auth_provider.get_token(auto_refresh=True)
39
+ if token is None:
40
+ exit_error(f"Not authenticated with {provider_name}.\nRun 'kstlib auth login {provider_name}' first.")
41
+
42
+ # Check if provider supports userinfo (OIDC)
43
+ if not hasattr(auth_provider, "get_userinfo"):
44
+ exit_error(
45
+ f"Provider '{provider_name}' does not support userinfo.\nUserinfo is only available for OIDC providers."
46
+ )
47
+
48
+ try:
49
+ userinfo = auth_provider.get_userinfo()
50
+ except Exception as e: # pylint: disable=broad-exception-caught
51
+ exit_error(f"Failed to fetch userinfo: {e}")
52
+
53
+ if raw:
54
+ print(to_json(userinfo))
55
+ return
56
+
57
+ if quiet:
58
+ # Show just the essential info
59
+ name = userinfo.get("name") or userinfo.get("preferred_username") or userinfo.get("sub", "unknown")
60
+ email = userinfo.get("email", "")
61
+ if email:
62
+ console.print(f"{name} <{email}>")
63
+ else:
64
+ console.print(name)
65
+ return
66
+
67
+ # Verbose output
68
+ table = Table(show_header=False, box=None, padding=(0, 2))
69
+ table.add_column("Field", style="dim")
70
+ table.add_column("Value")
71
+
72
+ # Standard OIDC claims in preferred order
73
+ claim_labels = {
74
+ "sub": "Subject",
75
+ "name": "Name",
76
+ "preferred_username": "Username",
77
+ "email": "Email",
78
+ "email_verified": "Email Verified",
79
+ "given_name": "Given Name",
80
+ "family_name": "Family Name",
81
+ "nickname": "Nickname",
82
+ "picture": "Picture",
83
+ "locale": "Locale",
84
+ "zoneinfo": "Timezone",
85
+ "updated_at": "Updated At",
86
+ }
87
+
88
+ # Show known claims first
89
+ for claim, label in claim_labels.items():
90
+ if claim in userinfo:
91
+ value = userinfo[claim]
92
+ if isinstance(value, bool):
93
+ value = "[green]Yes[/]" if value else "[red]No[/]"
94
+ elif claim == "updated_at" and isinstance(value, int):
95
+ value = format_timestamp(value)
96
+ table.add_row(label, str(value))
97
+
98
+ # Show any additional claims
99
+ for claim, value in userinfo.items():
100
+ if claim not in claim_labels:
101
+ table.add_row(claim, str(value))
102
+
103
+ console.print(Panel(table, title=f"User Info ({provider_name})", style="cyan"))
104
+
105
+
106
+ __all__ = ["whoami"]
@@ -0,0 +1,89 @@
1
+ """CLI commands for configuration management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pathlib
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from rich.panel import Panel
10
+
11
+ from kstlib.cli.common import console
12
+ from kstlib.config.export import (
13
+ ConfigExportError,
14
+ ConfigExportOptions,
15
+ export_configuration,
16
+ )
17
+
18
+ config_app = typer.Typer(help="Configuration utilities.")
19
+
20
+
21
+ @config_app.command("export", help="Export the default configuration file.")
22
+ def export_command(
23
+ section: Annotated[
24
+ str | None,
25
+ typer.Option(
26
+ "--section",
27
+ help="Optional dotted path selecting a subtree (e.g. utilities.secure_delete).",
28
+ ),
29
+ ] = None,
30
+ out: Annotated[
31
+ pathlib.Path | None,
32
+ typer.Option(
33
+ "--out",
34
+ help=(
35
+ "Destination file or directory. Defaults to ./kstlib.conf.yml when omitted. "
36
+ "When a directory is provided, the default filename is used."
37
+ ),
38
+ ),
39
+ ] = None,
40
+ stdout: Annotated[
41
+ bool,
42
+ typer.Option(
43
+ "--stdout",
44
+ help="Write configuration to stdout instead of a file.",
45
+ ),
46
+ ] = False,
47
+ force: Annotated[
48
+ bool,
49
+ typer.Option(
50
+ "--force",
51
+ help="Overwrite destination if it already exists.",
52
+ ),
53
+ ] = False,
54
+ ) -> None:
55
+ """Export default configuration to a file or stdout."""
56
+ resolved_out = out.expanduser() if isinstance(out, pathlib.Path) else out
57
+ options = ConfigExportOptions(section=section, out_path=resolved_out, stdout=stdout, force=force)
58
+
59
+ try:
60
+ result = export_configuration(options)
61
+ except ConfigExportError as exc:
62
+ console.print(f"[bold red]{exc}[/bold red]")
63
+ raise typer.Exit(code=1) from exc
64
+
65
+ if stdout:
66
+ if result.content is None:
67
+ console.print("[bold red]Export failed: empty content.[/bold red]")
68
+ raise typer.Exit(code=1)
69
+ console.print(result.content)
70
+ return
71
+
72
+ if result.destination is None:
73
+ console.print("[bold red]Export failed: missing destination file.[/bold red]")
74
+ raise typer.Exit(code=1)
75
+ console.print(
76
+ Panel.fit(
77
+ f"Configuration exported to [bold]{result.destination}[/bold]",
78
+ title="kstlib config export",
79
+ border_style="green",
80
+ )
81
+ )
82
+
83
+
84
+ def register_cli(app: typer.Typer) -> None:
85
+ """Register configuration subcommands on the main CLI application."""
86
+ app.add_typer(config_app, name="config")
87
+
88
+
89
+ __all__ = ["config_app", "register_cli"]
@@ -0,0 +1,39 @@
1
+ """CLI commands for session management (tmux/container)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from .attach import attach
8
+ from .list_sessions import list_sessions
9
+ from .logs import logs
10
+ from .start import start
11
+ from .status import status
12
+ from .stop import stop
13
+
14
+ ops_app = typer.Typer(help="Manage sessions (tmux/container).")
15
+
16
+ # Register commands on the ops_app
17
+ ops_app.command()(start)
18
+ ops_app.command()(stop)
19
+ ops_app.command()(attach)
20
+ ops_app.command()(status)
21
+ ops_app.command()(logs)
22
+ ops_app.command(name="list")(list_sessions)
23
+
24
+
25
+ def register_cli(app: typer.Typer) -> None:
26
+ """Register the ops sub-commands on the root Typer app."""
27
+ app.add_typer(ops_app, name="ops")
28
+
29
+
30
+ __all__ = [
31
+ "attach",
32
+ "list_sessions",
33
+ "logs",
34
+ "ops_app",
35
+ "register_cli",
36
+ "start",
37
+ "status",
38
+ "stop",
39
+ ]
@@ -0,0 +1,49 @@
1
+ """Attach to a session."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from kstlib.cli.common import exit_error
6
+ from kstlib.ops.exceptions import OpsError, SessionNotFoundError
7
+
8
+ from .common import (
9
+ BACKEND_OPTION,
10
+ SESSION_ARGUMENT,
11
+ get_session_manager,
12
+ )
13
+
14
+
15
+ def attach(
16
+ name: str = SESSION_ARGUMENT,
17
+ backend: str | None = BACKEND_OPTION,
18
+ ) -> None:
19
+ """Attach to a running session.
20
+
21
+ Attaches the current terminal to a tmux session or container.
22
+ This command replaces the current process.
23
+
24
+ For tmux: Use Ctrl+B D to detach.
25
+ For container: Use Ctrl+P Ctrl+Q to detach.
26
+
27
+ Examples:
28
+ kstlib ops attach dev
29
+ kstlib ops attach prod --backend container
30
+ """
31
+ try:
32
+ manager = get_session_manager(name, backend=backend)
33
+
34
+ if not manager.exists():
35
+ exit_error(f"Session '{name}' not found.")
36
+
37
+ if not manager.is_running():
38
+ exit_error(f"Session '{name}' is not running.")
39
+
40
+ # This replaces the current process and does not return
41
+ manager.attach()
42
+
43
+ except SessionNotFoundError:
44
+ exit_error(f"Session '{name}' not found.")
45
+ except OpsError as e:
46
+ exit_error(str(e))
47
+
48
+
49
+ __all__ = ["attach"]
@@ -0,0 +1,269 @@
1
+ """Shared options and utilities for ops CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import typer
8
+
9
+ from kstlib.cli.common import CommandResult, CommandStatus, exit_error
10
+ from kstlib.ops import SessionManager
11
+ from kstlib.ops.exceptions import OpsError, SessionAmbiguousError
12
+ from kstlib.ops.manager import auto_detect_backend
13
+
14
+ # ============================================================================
15
+ # Shared Arguments and Options
16
+ # ============================================================================
17
+
18
+
19
+ SESSION_ARGUMENT = typer.Argument(
20
+ ...,
21
+ help="Session name.",
22
+ metavar="NAME",
23
+ )
24
+
25
+ BACKEND_OPTION = typer.Option(
26
+ None,
27
+ "--backend",
28
+ "-b",
29
+ help="Backend type (tmux or container). If not specified, uses config default.",
30
+ )
31
+
32
+ IMAGE_OPTION = typer.Option(
33
+ None,
34
+ "--image",
35
+ "-i",
36
+ help="Container image (container backend only).",
37
+ )
38
+
39
+ COMMAND_OPTION = typer.Option(
40
+ None,
41
+ "--command",
42
+ "-c",
43
+ help="Command to run in the session.",
44
+ )
45
+
46
+ QUIET_OPTION = typer.Option(
47
+ False,
48
+ "--quiet",
49
+ "-q",
50
+ help="Minimal output.",
51
+ )
52
+
53
+ JSON_OPTION = typer.Option(
54
+ False,
55
+ "--json",
56
+ help="Output in JSON format.",
57
+ )
58
+
59
+ WORKDIR_OPTION = typer.Option(
60
+ None,
61
+ "--workdir",
62
+ "-w",
63
+ help="Working directory for the session.",
64
+ )
65
+
66
+ ENV_OPTION = typer.Option(
67
+ None,
68
+ "--env",
69
+ "-e",
70
+ help="Environment variable (KEY=VALUE). Can be repeated.",
71
+ )
72
+
73
+ VOLUME_OPTION = typer.Option(
74
+ None,
75
+ "--volume",
76
+ "-v",
77
+ help="Volume mount (host:container). Can be repeated.",
78
+ )
79
+
80
+ PORT_OPTION = typer.Option(
81
+ None,
82
+ "--port",
83
+ "-p",
84
+ help="Port mapping (host:container). Can be repeated.",
85
+ )
86
+
87
+ FORCE_OPTION = typer.Option(
88
+ False,
89
+ "--force",
90
+ "-f",
91
+ help="Force stop without graceful shutdown.",
92
+ )
93
+
94
+ TIMEOUT_OPTION = typer.Option(
95
+ 10,
96
+ "--timeout",
97
+ "-t",
98
+ help="Seconds to wait for graceful shutdown.",
99
+ )
100
+
101
+ LINES_OPTION = typer.Option(
102
+ 100,
103
+ "--lines",
104
+ "-n",
105
+ help="Number of lines to show.",
106
+ )
107
+
108
+
109
+ # ============================================================================
110
+ # Helper Functions
111
+ # ============================================================================
112
+
113
+
114
+ def _rebuild_with_overrides(
115
+ name: str,
116
+ manager: SessionManager,
117
+ *,
118
+ backend: str | None,
119
+ image: str | None,
120
+ command: str | None,
121
+ ) -> SessionManager:
122
+ """Rebuild a config-loaded SessionManager with CLI overrides.
123
+
124
+ Args:
125
+ name: Session name.
126
+ manager: Original config-loaded manager.
127
+ backend: CLI backend override (or None).
128
+ image: CLI image override (or None).
129
+ command: CLI command override (or None).
130
+
131
+ Returns:
132
+ New SessionManager with overrides applied.
133
+ """
134
+ cfg = manager.config
135
+ kwargs: dict[str, Any] = {
136
+ "backend": backend or cfg.backend.value,
137
+ "command": command or cfg.command,
138
+ "working_dir": cfg.working_dir,
139
+ "env": cfg.env,
140
+ "image": image or cfg.image,
141
+ "volumes": list(cfg.volumes),
142
+ "ports": list(cfg.ports),
143
+ "runtime": cfg.runtime,
144
+ "log_volume": cfg.log_volume,
145
+ }
146
+ return SessionManager(name, **kwargs)
147
+
148
+
149
+ def _try_from_config(
150
+ name: str,
151
+ *,
152
+ backend: str | None,
153
+ image: str | None,
154
+ command: str | None,
155
+ ) -> SessionManager | None:
156
+ """Try to load a SessionManager from config, with CLI overrides.
157
+
158
+ Args:
159
+ name: Session name.
160
+ backend: CLI backend override (or None).
161
+ image: CLI image override (or None).
162
+ command: CLI command override (or None).
163
+
164
+ Returns:
165
+ SessionManager if found in config, None otherwise.
166
+ """
167
+ try:
168
+ manager = SessionManager.from_config(name)
169
+ except OpsError:
170
+ return None
171
+ if backend or image or command:
172
+ return _rebuild_with_overrides(
173
+ name,
174
+ manager,
175
+ backend=backend,
176
+ image=image,
177
+ command=command,
178
+ )
179
+ return manager
180
+
181
+
182
+ def get_session_manager(
183
+ name: str,
184
+ *,
185
+ backend: str | None = None,
186
+ image: str | None = None,
187
+ command: str | None = None,
188
+ from_config: bool = True,
189
+ ) -> SessionManager:
190
+ """Get or create a SessionManager.
191
+
192
+ Tries to load session configuration from kstlib.conf.yml first,
193
+ then applies CLI arguments as overrides. Falls back to auto-detection
194
+ or explicit options if the session is not defined in config.
195
+
196
+ Args:
197
+ name: Session name.
198
+ backend: Override backend type.
199
+ image: Container image (for container backend).
200
+ command: Command to run.
201
+ from_config: If True, try to load from config first.
202
+
203
+ Returns:
204
+ SessionManager instance.
205
+
206
+ Raises:
207
+ typer.Exit: On configuration error or ambiguous session.
208
+ """
209
+ try:
210
+ # Try to load from config first (CLI args override config values)
211
+ if from_config:
212
+ manager = _try_from_config(name, backend=backend, image=image, command=command)
213
+ if manager is not None:
214
+ return manager
215
+
216
+ # Auto-detect backend if not specified
217
+ if backend is None:
218
+ detected = auto_detect_backend(name)
219
+ if detected is not None:
220
+ backend = detected.value
221
+
222
+ # Create with explicit options
223
+ kwargs: dict[str, Any] = {}
224
+ if backend:
225
+ kwargs["backend"] = backend
226
+ if image:
227
+ kwargs["image"] = image
228
+ if command:
229
+ kwargs["command"] = command
230
+
231
+ return SessionManager(name, **kwargs)
232
+ except SessionAmbiguousError as e:
233
+ exit_error(str(e))
234
+ except OpsError as e:
235
+ exit_error(str(e))
236
+
237
+
238
+ def handle_ops_error(e: OpsError) -> CommandResult:
239
+ """Convert an OpsError to a CommandResult.
240
+
241
+ Args:
242
+ e: The OpsError exception.
243
+
244
+ Returns:
245
+ CommandResult with error status.
246
+ """
247
+ return CommandResult(
248
+ status=CommandStatus.ERROR,
249
+ message=str(e),
250
+ )
251
+
252
+
253
+ __all__ = [
254
+ "BACKEND_OPTION",
255
+ "COMMAND_OPTION",
256
+ "ENV_OPTION",
257
+ "FORCE_OPTION",
258
+ "IMAGE_OPTION",
259
+ "JSON_OPTION",
260
+ "LINES_OPTION",
261
+ "PORT_OPTION",
262
+ "QUIET_OPTION",
263
+ "SESSION_ARGUMENT",
264
+ "TIMEOUT_OPTION",
265
+ "VOLUME_OPTION",
266
+ "WORKDIR_OPTION",
267
+ "get_session_manager",
268
+ "handle_ops_error",
269
+ ]