kctl-dbgate 0.3.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.
- kctl_dbgate/__init__.py +1 -0
- kctl_dbgate/__main__.py +3 -0
- kctl_dbgate/cli.py +94 -0
- kctl_dbgate/commands/__init__.py +0 -0
- kctl_dbgate/commands/config_cmd.py +417 -0
- kctl_dbgate/commands/connections.py +572 -0
- kctl_dbgate/commands/doctor_cmd.py +171 -0
- kctl_dbgate/commands/health.py +67 -0
- kctl_dbgate/commands/history.py +80 -0
- kctl_dbgate/commands/plugins.py +96 -0
- kctl_dbgate/commands/query.py +197 -0
- kctl_dbgate/commands/servers.py +139 -0
- kctl_dbgate/commands/sessions.py +72 -0
- kctl_dbgate/commands/skill_cmd.py +74 -0
- kctl_dbgate/commands/storage.py +36 -0
- kctl_dbgate/core/__init__.py +0 -0
- kctl_dbgate/core/callbacks.py +32 -0
- kctl_dbgate/core/client.py +172 -0
- kctl_dbgate/core/config.py +134 -0
- kctl_dbgate/core/exceptions.py +21 -0
- kctl_dbgate/core/plugins.py +13 -0
- kctl_dbgate-0.3.0.dist-info/METADATA +289 -0
- kctl_dbgate-0.3.0.dist-info/RECORD +25 -0
- kctl_dbgate-0.3.0.dist-info/WHEEL +4 -0
- kctl_dbgate-0.3.0.dist-info/entry_points.txt +2 -0
kctl_dbgate/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
kctl_dbgate/__main__.py
ADDED
kctl_dbgate/cli.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Main CLI entry point for kctl-dbgate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from kctl_lib import handle_cli_error # noqa: F401
|
|
9
|
+
from kctl_lib.self_update import notify_if_outdated
|
|
10
|
+
|
|
11
|
+
from kctl_dbgate import __version__
|
|
12
|
+
from kctl_dbgate.commands.config_cmd import app as config_app
|
|
13
|
+
from kctl_dbgate.commands.connections import app as connections_app
|
|
14
|
+
from kctl_dbgate.commands.doctor_cmd import app as doctor_app
|
|
15
|
+
from kctl_dbgate.commands.health import app as health_app
|
|
16
|
+
from kctl_dbgate.commands.history import app as history_app
|
|
17
|
+
from kctl_dbgate.commands.plugins import app as plugins_app
|
|
18
|
+
from kctl_dbgate.commands.query import app as query_app
|
|
19
|
+
from kctl_dbgate.commands.servers import app as servers_app
|
|
20
|
+
from kctl_dbgate.commands.sessions import app as sessions_app
|
|
21
|
+
from kctl_dbgate.commands.skill_cmd import app as skill_app
|
|
22
|
+
from kctl_dbgate.commands.storage import app as storage_app
|
|
23
|
+
from kctl_dbgate.core.callbacks import AppContext
|
|
24
|
+
from kctl_dbgate.core.plugins import discover_and_load_plugins
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def version_callback(value: bool) -> None:
|
|
28
|
+
if value:
|
|
29
|
+
typer.echo(f"kctl-dbgate {__version__}")
|
|
30
|
+
raise typer.Exit()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
app = typer.Typer(
|
|
34
|
+
name="kctl-dbgate",
|
|
35
|
+
help="Kodemeio DBGate CLI - manage your DBGate deployment.",
|
|
36
|
+
no_args_is_help=True,
|
|
37
|
+
rich_markup_mode="rich",
|
|
38
|
+
pretty_exceptions_enable=False,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.callback()
|
|
43
|
+
def main(
|
|
44
|
+
ctx: typer.Context,
|
|
45
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
|
|
46
|
+
quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
|
|
47
|
+
profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile name")] = None,
|
|
48
|
+
format: Annotated[str, typer.Option("--format", "-f", help="Output format: pretty, json, csv, yaml")] = "pretty",
|
|
49
|
+
no_header: Annotated[bool, typer.Option("--no-header", help="Omit header row in CSV output")] = False,
|
|
50
|
+
url: Annotated[str | None, typer.Option("--url", help="DBGate URL override")] = None,
|
|
51
|
+
login: Annotated[str | None, typer.Option("--login", help="UI login override")] = None,
|
|
52
|
+
password: Annotated[str | None, typer.Option("--password", help="UI password override")] = None,
|
|
53
|
+
version: Annotated[
|
|
54
|
+
bool, typer.Option("--version", "-V", callback=version_callback, is_eager=True, help="Show version")
|
|
55
|
+
] = False,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Kodemeio DBGate CLI."""
|
|
58
|
+
ctx.ensure_object(dict)
|
|
59
|
+
ctx.obj = AppContext(
|
|
60
|
+
json_mode=json_output,
|
|
61
|
+
quiet=quiet,
|
|
62
|
+
profile=profile,
|
|
63
|
+
format=format,
|
|
64
|
+
no_header=no_header,
|
|
65
|
+
url_override=url,
|
|
66
|
+
login_override=login,
|
|
67
|
+
password_override=password,
|
|
68
|
+
)
|
|
69
|
+
notify_if_outdated(ctx.obj.output, "kctl-dbgate", __version__)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Register command groups
|
|
73
|
+
app.add_typer(config_app, name="config")
|
|
74
|
+
app.add_typer(health_app, name="health")
|
|
75
|
+
app.add_typer(history_app, name="history")
|
|
76
|
+
app.add_typer(connections_app, name="connections")
|
|
77
|
+
app.add_typer(doctor_app, name="doctor")
|
|
78
|
+
app.add_typer(plugins_app, name="plugins")
|
|
79
|
+
app.add_typer(query_app, name="query")
|
|
80
|
+
app.add_typer(servers_app, name="servers")
|
|
81
|
+
app.add_typer(sessions_app, name="sessions")
|
|
82
|
+
app.add_typer(skill_app, name="skill")
|
|
83
|
+
app.add_typer(storage_app, name="storage")
|
|
84
|
+
|
|
85
|
+
# Load any third-party plugins
|
|
86
|
+
discover_and_load_plugins(app)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _run() -> None:
|
|
90
|
+
app()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
_run()
|
|
File without changes
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""Configuration management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated, Any
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
import yaml
|
|
10
|
+
from kctl_lib.exceptions import KctlError
|
|
11
|
+
|
|
12
|
+
from kctl_dbgate.core.callbacks import AppContext
|
|
13
|
+
from kctl_dbgate.core.client import DBGateClient
|
|
14
|
+
from kctl_dbgate.core.config import (
|
|
15
|
+
CONFIG_FILE,
|
|
16
|
+
SERVICE_KEY,
|
|
17
|
+
ServiceConfig,
|
|
18
|
+
get_all_services_in_profile,
|
|
19
|
+
get_default_profile,
|
|
20
|
+
get_profile_names,
|
|
21
|
+
get_service_config,
|
|
22
|
+
load_raw_config,
|
|
23
|
+
remove_profile,
|
|
24
|
+
resolve_active_profile_name,
|
|
25
|
+
save_raw_config,
|
|
26
|
+
set_default_profile,
|
|
27
|
+
set_service_config,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
app = typer.Typer(help="Manage CLI configuration and profiles.")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _mask(secret: str) -> str:
|
|
34
|
+
if not secret:
|
|
35
|
+
return "[dim]not set[/dim]"
|
|
36
|
+
if len(secret) <= 8:
|
|
37
|
+
return "****"
|
|
38
|
+
return f"{secret[:4]}{'*' * (len(secret) - 8)}{secret[-4:]}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _test_connection(url: str, login: str, password: str) -> tuple[bool, str]:
|
|
42
|
+
try:
|
|
43
|
+
client = DBGateClient(base_url=url, login=login, password=password)
|
|
44
|
+
client.login()
|
|
45
|
+
client.close()
|
|
46
|
+
return True, "authenticated"
|
|
47
|
+
except Exception as e:
|
|
48
|
+
return False, str(e)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command()
|
|
52
|
+
def init(
|
|
53
|
+
ctx: typer.Context,
|
|
54
|
+
url: Annotated[str | None, typer.Option("--url", help="DBGate base URL")] = None,
|
|
55
|
+
login: Annotated[str | None, typer.Option("--login", help="UI username")] = None,
|
|
56
|
+
password: Annotated[str | None, typer.Option("--password", help="UI password")] = None,
|
|
57
|
+
name: Annotated[str | None, typer.Option("--name", "-n", help="Profile name")] = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Initialize CLI configuration (interactive if no flags given)."""
|
|
60
|
+
actx: AppContext = ctx.obj
|
|
61
|
+
out = actx.output
|
|
62
|
+
|
|
63
|
+
profile_name = name or typer.prompt("Profile name", default="kodemeio")
|
|
64
|
+
api_url = url or typer.prompt("DBGate URL (e.g. https://dbgate.kodeme.io)")
|
|
65
|
+
api_login = login or typer.prompt("Login", default="admin")
|
|
66
|
+
api_password = password or typer.prompt("Password", hide_input=True)
|
|
67
|
+
|
|
68
|
+
out.info(f"Testing connection to {api_url}...")
|
|
69
|
+
ok, info = _test_connection(api_url, api_login, api_password)
|
|
70
|
+
if ok:
|
|
71
|
+
out.success(f"Connected: {info}")
|
|
72
|
+
else:
|
|
73
|
+
out.error(f"Connection failed: {info}")
|
|
74
|
+
if not typer.confirm("Save configuration anyway?", default=False):
|
|
75
|
+
raise typer.Exit(code=1)
|
|
76
|
+
|
|
77
|
+
svc = ServiceConfig(url=api_url, login=api_login, password=api_password)
|
|
78
|
+
set_service_config(profile_name, svc)
|
|
79
|
+
|
|
80
|
+
if len(get_profile_names()) <= 1:
|
|
81
|
+
set_default_profile(profile_name)
|
|
82
|
+
|
|
83
|
+
out.success(f"Configuration saved to {CONFIG_FILE}")
|
|
84
|
+
out.kv("Profile", profile_name)
|
|
85
|
+
out.kv("Service", SERVICE_KEY)
|
|
86
|
+
out.kv("URL", api_url)
|
|
87
|
+
out.kv("Login", api_login)
|
|
88
|
+
out.kv("Password", _mask(api_password))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@app.command()
|
|
92
|
+
def show(ctx: typer.Context) -> None:
|
|
93
|
+
"""Show full configuration (secrets masked)."""
|
|
94
|
+
actx: AppContext = ctx.obj
|
|
95
|
+
out = actx.output
|
|
96
|
+
|
|
97
|
+
default = get_default_profile()
|
|
98
|
+
profiles = get_profile_names()
|
|
99
|
+
|
|
100
|
+
if out.json_mode:
|
|
101
|
+
data = load_raw_config()
|
|
102
|
+
for _pname, pdata in data.get("profiles", {}).items():
|
|
103
|
+
for _svc, svc_data in pdata.items():
|
|
104
|
+
if isinstance(svc_data, dict) and "password" in svc_data:
|
|
105
|
+
svc_data["password"] = _mask(svc_data["password"])
|
|
106
|
+
out.raw_json(data)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
sections: list[tuple[str, list[tuple[str, str]]]] = []
|
|
110
|
+
sections.append(
|
|
111
|
+
(
|
|
112
|
+
"General",
|
|
113
|
+
[
|
|
114
|
+
("Config file", str(CONFIG_FILE)),
|
|
115
|
+
("Default profile", default),
|
|
116
|
+
("Total profiles", str(len(profiles))),
|
|
117
|
+
("This CLI", f"kctl-dbgate -> service key: {SERVICE_KEY}"),
|
|
118
|
+
],
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
for pname in profiles:
|
|
123
|
+
marker = " [green](default)[/green]" if pname == default else ""
|
|
124
|
+
services = get_all_services_in_profile(pname)
|
|
125
|
+
|
|
126
|
+
kvs: list[tuple[str, str]] = []
|
|
127
|
+
for svc_name, svc_data in services.items():
|
|
128
|
+
if not isinstance(svc_data, dict):
|
|
129
|
+
continue
|
|
130
|
+
svc_url = svc_data.get("url", "")
|
|
131
|
+
svc_login = svc_data.get("login", "")
|
|
132
|
+
svc_pwd = _mask(svc_data.get("password", ""))
|
|
133
|
+
indicator = "[green]●[/green]" if svc_name == SERVICE_KEY else "[dim]○[/dim]"
|
|
134
|
+
summary = f"{svc_url} user: {svc_login} password: {svc_pwd}" if svc_name == SERVICE_KEY else svc_url
|
|
135
|
+
kvs.append((f"{indicator} {svc_name}", summary))
|
|
136
|
+
|
|
137
|
+
if not kvs:
|
|
138
|
+
kvs.append(("(empty)", "no services configured"))
|
|
139
|
+
|
|
140
|
+
sections.append((f"Profile: {pname}{marker}", kvs))
|
|
141
|
+
|
|
142
|
+
out.detail("Configuration", sections)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@app.command("set")
|
|
146
|
+
def set_(
|
|
147
|
+
ctx: typer.Context,
|
|
148
|
+
key: Annotated[str, typer.Argument(help="Config key: url, login, password, or default_profile")],
|
|
149
|
+
value: Annotated[str, typer.Argument(help="Value to set")],
|
|
150
|
+
profile_arg: Annotated[str | None, typer.Option("--profile-name", help="Target profile")] = None,
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Set a configuration value for the current service."""
|
|
153
|
+
actx: AppContext = ctx.obj
|
|
154
|
+
out = actx.output
|
|
155
|
+
|
|
156
|
+
if key == "default_profile":
|
|
157
|
+
set_default_profile(value)
|
|
158
|
+
out.success(f"Default profile set to: {value}")
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
valid_fields = {"url", "login", "password"}
|
|
162
|
+
if key not in valid_fields:
|
|
163
|
+
out.error(f"Unknown key: {key}")
|
|
164
|
+
out.info(f"Valid keys: {', '.join(sorted(valid_fields))}, default_profile")
|
|
165
|
+
raise typer.Exit(1)
|
|
166
|
+
|
|
167
|
+
pname = profile_arg or resolve_active_profile_name(actx.profile)
|
|
168
|
+
svc = get_service_config(pname)
|
|
169
|
+
setattr(svc, key, value)
|
|
170
|
+
set_service_config(pname, svc)
|
|
171
|
+
|
|
172
|
+
display = _mask(value) if key == "password" else value
|
|
173
|
+
out.success(f"[{pname}] {SERVICE_KEY}.{key} = {display}")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@app.command()
|
|
177
|
+
def use(
|
|
178
|
+
ctx: typer.Context,
|
|
179
|
+
name: Annotated[str, typer.Argument(help="Profile name to switch to")],
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Switch the default profile."""
|
|
182
|
+
actx: AppContext = ctx.obj
|
|
183
|
+
out = actx.output
|
|
184
|
+
|
|
185
|
+
profiles = get_profile_names()
|
|
186
|
+
if name not in profiles:
|
|
187
|
+
out.error(f"Profile '{name}' not found")
|
|
188
|
+
out.info(f"Available: {', '.join(profiles)}")
|
|
189
|
+
raise typer.Exit(1)
|
|
190
|
+
|
|
191
|
+
old_default = get_default_profile()
|
|
192
|
+
set_default_profile(name)
|
|
193
|
+
|
|
194
|
+
svc = get_service_config(name)
|
|
195
|
+
if svc.url:
|
|
196
|
+
ok, info = _test_connection(svc.url, svc.login, svc.password)
|
|
197
|
+
if ok:
|
|
198
|
+
out.success(f"Switched to '{name}' ({svc.url}) — {info}")
|
|
199
|
+
else:
|
|
200
|
+
out.warn(f"Switched to '{name}' ({svc.url}) — {info}")
|
|
201
|
+
else:
|
|
202
|
+
out.warn(f"Switched to '{name}' — no {SERVICE_KEY} config in this profile")
|
|
203
|
+
|
|
204
|
+
out.info(f"Previous default: {old_default}")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@app.command()
|
|
208
|
+
def remove(
|
|
209
|
+
ctx: typer.Context,
|
|
210
|
+
name: Annotated[str, typer.Argument(help="Profile name to remove")],
|
|
211
|
+
force: Annotated[bool, typer.Option("--force", help="Skip confirmation")] = False,
|
|
212
|
+
service_only: Annotated[bool, typer.Option("--service-only", help=f"Only remove {SERVICE_KEY} config")] = False,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Remove a profile or just its DBGate config."""
|
|
215
|
+
actx: AppContext = ctx.obj
|
|
216
|
+
out = actx.output
|
|
217
|
+
|
|
218
|
+
profiles = get_profile_names()
|
|
219
|
+
if name not in profiles:
|
|
220
|
+
out.error(f"Profile '{name}' not found")
|
|
221
|
+
raise typer.Exit(1)
|
|
222
|
+
|
|
223
|
+
if service_only:
|
|
224
|
+
if not force:
|
|
225
|
+
svc = get_service_config(name)
|
|
226
|
+
if not typer.confirm(f"Remove {SERVICE_KEY} config from '{name}' ({svc.url})?"):
|
|
227
|
+
raise typer.Exit(0)
|
|
228
|
+
data = load_raw_config()
|
|
229
|
+
profile = data.get("profiles", {}).get(name, {})
|
|
230
|
+
profile.pop(SERVICE_KEY, None)
|
|
231
|
+
save_raw_config(data)
|
|
232
|
+
out.success(f"Removed {SERVICE_KEY} config from profile '{name}'")
|
|
233
|
+
else:
|
|
234
|
+
if not force:
|
|
235
|
+
services = get_all_services_in_profile(name)
|
|
236
|
+
svc_list = ", ".join(services.keys())
|
|
237
|
+
if not typer.confirm(f"Remove entire profile '{name}' (services: {svc_list})?"):
|
|
238
|
+
raise typer.Exit(0)
|
|
239
|
+
remove_profile(name)
|
|
240
|
+
out.success(f"Profile '{name}' removed")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@app.command()
|
|
244
|
+
def profiles(ctx: typer.Context) -> None:
|
|
245
|
+
"""List all profiles with DBGate connection status."""
|
|
246
|
+
actx: AppContext = ctx.obj
|
|
247
|
+
out = actx.output
|
|
248
|
+
|
|
249
|
+
profile_names = get_profile_names()
|
|
250
|
+
if not profile_names:
|
|
251
|
+
out.warn("No profiles configured. Run: kctl-dbgate config init")
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
active = resolve_active_profile_name(actx.profile)
|
|
255
|
+
default = get_default_profile()
|
|
256
|
+
|
|
257
|
+
rows: list[list[str]] = []
|
|
258
|
+
for pname in profile_names:
|
|
259
|
+
svc = get_service_config(pname)
|
|
260
|
+
is_active = pname == active
|
|
261
|
+
status_marker = "[green]active[/green]" if is_active else ("default" if pname == default else "")
|
|
262
|
+
if svc.url:
|
|
263
|
+
ok, info = _test_connection(svc.url, svc.login, svc.password)
|
|
264
|
+
conn = f"[green]{info}[/green]" if ok else f"[red]{info[:40]}[/red]"
|
|
265
|
+
else:
|
|
266
|
+
conn = f"[dim]no {SERVICE_KEY} config[/dim]"
|
|
267
|
+
rows.append([pname, svc.url or "-", svc.login or "-", conn, status_marker])
|
|
268
|
+
|
|
269
|
+
out.table(
|
|
270
|
+
"Profiles",
|
|
271
|
+
[("Name", "cyan"), ("DBGate URL", ""), ("Login", ""), ("Status", ""), ("", "green")],
|
|
272
|
+
rows,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@app.command()
|
|
277
|
+
def current(ctx: typer.Context) -> None:
|
|
278
|
+
"""Show the active profile and connection status."""
|
|
279
|
+
actx: AppContext = ctx.obj
|
|
280
|
+
out = actx.output
|
|
281
|
+
|
|
282
|
+
active = resolve_active_profile_name(actx.profile)
|
|
283
|
+
svc = get_service_config(active)
|
|
284
|
+
|
|
285
|
+
out.kv("Active profile", active)
|
|
286
|
+
out.kv("Default profile", get_default_profile())
|
|
287
|
+
out.kv("URL", svc.url or "[dim]not set[/dim]")
|
|
288
|
+
out.kv("Login", svc.login or "[dim]not set[/dim]")
|
|
289
|
+
out.kv("Password", _mask(svc.password))
|
|
290
|
+
|
|
291
|
+
if svc.url:
|
|
292
|
+
ok, info = _test_connection(svc.url, svc.login, svc.password)
|
|
293
|
+
if ok:
|
|
294
|
+
out.success(f"Connection OK — {info}")
|
|
295
|
+
else:
|
|
296
|
+
out.error(f"Connection failed: {info}")
|
|
297
|
+
else:
|
|
298
|
+
out.warn(f"No {SERVICE_KEY} config in active profile. Run: kctl-dbgate config init")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ----------------------------------------------------------------------
|
|
302
|
+
# Server-side settings (POST /config/*)
|
|
303
|
+
# ----------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@app.command("server-show")
|
|
307
|
+
def server_show(ctx: typer.Context) -> None:
|
|
308
|
+
"""Show DBGate's server-side configuration (POST /config/get)."""
|
|
309
|
+
actx: AppContext = ctx.obj
|
|
310
|
+
out = actx.output
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
cfg = actx.client.call("/config/get")
|
|
314
|
+
except KctlError as e:
|
|
315
|
+
out.error(str(e))
|
|
316
|
+
raise typer.Exit(1) from e
|
|
317
|
+
|
|
318
|
+
if not isinstance(cfg, dict):
|
|
319
|
+
out.error(f"Unexpected response: {cfg!r}")
|
|
320
|
+
raise typer.Exit(1)
|
|
321
|
+
|
|
322
|
+
rows = [[str(k), str(v)] for k, v in sorted(cfg.items())]
|
|
323
|
+
out.table(
|
|
324
|
+
title="DBGate server configuration",
|
|
325
|
+
columns=[("Key", "cyan"), ("Value", "")],
|
|
326
|
+
rows=rows,
|
|
327
|
+
data_for_json=[cfg],
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@app.command("server-set")
|
|
332
|
+
def server_set(
|
|
333
|
+
ctx: typer.Context,
|
|
334
|
+
key: Annotated[str, typer.Option("--key", help="Setting key")],
|
|
335
|
+
value: Annotated[str, typer.Option("--value", help="Setting value")],
|
|
336
|
+
) -> None:
|
|
337
|
+
"""Update a single server-side setting (POST /config/update-settings)."""
|
|
338
|
+
actx: AppContext = ctx.obj
|
|
339
|
+
out = actx.output
|
|
340
|
+
payload: dict[str, Any] = {key: value}
|
|
341
|
+
try:
|
|
342
|
+
actx.client.call("/config/update-settings", payload)
|
|
343
|
+
except KctlError as e:
|
|
344
|
+
out.error(str(e))
|
|
345
|
+
raise typer.Exit(1) from e
|
|
346
|
+
out.success(f"Setting {key} updated")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@app.command("server-changelog")
|
|
350
|
+
def server_changelog(ctx: typer.Context) -> None:
|
|
351
|
+
"""Show DBGate server changelog (POST /config/changelog)."""
|
|
352
|
+
actx: AppContext = ctx.obj
|
|
353
|
+
out = actx.output
|
|
354
|
+
try:
|
|
355
|
+
result = actx.client.call("/config/changelog")
|
|
356
|
+
except KctlError as e:
|
|
357
|
+
out.error(str(e))
|
|
358
|
+
raise typer.Exit(1) from e
|
|
359
|
+
if isinstance(result, str):
|
|
360
|
+
out.text(result)
|
|
361
|
+
else:
|
|
362
|
+
out.text(str(result))
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@app.command("server-export")
|
|
366
|
+
def server_export(
|
|
367
|
+
ctx: typer.Context,
|
|
368
|
+
outfile: Annotated[Path, typer.Argument(help="Path to write YAML dump to")],
|
|
369
|
+
) -> None:
|
|
370
|
+
"""Dump the server's /config/get response to a YAML file.
|
|
371
|
+
|
|
372
|
+
NOTE: DBGate exposes no true export endpoint, so this dumps the
|
|
373
|
+
read-only config snapshot. Use `server-import` with a file from
|
|
374
|
+
`import-connections-and-settings` producer if available.
|
|
375
|
+
"""
|
|
376
|
+
actx: AppContext = ctx.obj
|
|
377
|
+
out = actx.output
|
|
378
|
+
try:
|
|
379
|
+
cfg = actx.client.call("/config/get")
|
|
380
|
+
except KctlError as e:
|
|
381
|
+
out.error(str(e))
|
|
382
|
+
raise typer.Exit(1) from e
|
|
383
|
+
outfile.write_text(yaml.safe_dump(cfg, sort_keys=True))
|
|
384
|
+
out.success(f"Server config exported to {outfile}")
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@app.command("server-import")
|
|
388
|
+
def server_import(
|
|
389
|
+
ctx: typer.Context,
|
|
390
|
+
infile: Annotated[Path, typer.Argument(help="YAML file to import")],
|
|
391
|
+
dry_run: Annotated[bool, typer.Option("--dry-run", help="Don't send; just show what would be sent")] = False,
|
|
392
|
+
) -> None:
|
|
393
|
+
"""Import connections + settings from a YAML file.
|
|
394
|
+
|
|
395
|
+
Calls POST /config/import-connections-and-settings.
|
|
396
|
+
"""
|
|
397
|
+
actx: AppContext = ctx.obj
|
|
398
|
+
out = actx.output
|
|
399
|
+
|
|
400
|
+
if not infile.exists():
|
|
401
|
+
out.error(f"File not found: {infile}")
|
|
402
|
+
raise typer.Exit(1)
|
|
403
|
+
|
|
404
|
+
payload = yaml.safe_load(infile.read_text()) or {}
|
|
405
|
+
|
|
406
|
+
if dry_run:
|
|
407
|
+
out.header("Dry-run payload")
|
|
408
|
+
out.text(yaml.safe_dump(payload, sort_keys=True))
|
|
409
|
+
out.info("No API call made (--dry-run)")
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
actx.client.call("/config/import-connections-and-settings", payload)
|
|
414
|
+
except KctlError as e:
|
|
415
|
+
out.error(str(e))
|
|
416
|
+
raise typer.Exit(1) from e
|
|
417
|
+
out.success(f"Imported from {infile}")
|