pucit 0.1.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.
pucit/__about__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
pucit/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from pucit.__about__ import __version__
2
+
3
+ __all__ = ["__version__"]
pucit/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from pucit.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
pucit/cli.py ADDED
@@ -0,0 +1,258 @@
1
+ """pucit — PUCIT student toolkit CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import List, Optional
7
+
8
+ import typer
9
+
10
+ from pucit import __version__
11
+ from pucit import cpp_build
12
+ from pucit import docker_util as d
13
+ from pucit.commands.bypass import bypass_app
14
+ from pucit.commands.doctor import run_doctor, run_list
15
+ from pucit.commands.install import install_app
16
+ from pucit.commands.oracle import oracle_app
17
+ from pucit.commands.scaffold import init_project, new_file
18
+ from pucit.util import fail, first_existing, info, ok, run_cmd, run_streaming, which
19
+
20
+ app = typer.Typer(
21
+ name="pucit",
22
+ help="PUCIT student toolkit — Oracle, PF/C++, bypass, and lab ease commands.",
23
+ no_args_is_help=True,
24
+ add_completion=False,
25
+ )
26
+
27
+ app.add_typer(install_app, name="install")
28
+ app.add_typer(bypass_app, name="bypass")
29
+ app.add_typer(oracle_app, name="oracle")
30
+
31
+
32
+ def _oracle_target(target: str) -> None:
33
+ if target.lower() not in {"oracle", "db", "oracledb"}:
34
+ fail(f"Unknown target '{target}'. Try: oracle")
35
+ raise typer.Exit(1)
36
+
37
+
38
+ @app.command("start")
39
+ def start_cmd(target: str = typer.Argument(..., help="Service to start (oracle)")) -> None:
40
+ """Start a managed service: pucit start oracle"""
41
+ _oracle_target(target)
42
+ from pucit.commands.oracle import oracle_start
43
+
44
+ oracle_start()
45
+
46
+
47
+ @app.command("stop")
48
+ def stop_cmd(target: str = typer.Argument(..., help="Service to stop (oracle)")) -> None:
49
+ """Stop a managed service: pucit stop oracle"""
50
+ _oracle_target(target)
51
+ from pucit.commands.oracle import oracle_stop
52
+
53
+ oracle_stop()
54
+
55
+
56
+ @app.command("status")
57
+ def status_cmd(target: str = typer.Argument("oracle", help="Service (oracle)")) -> None:
58
+ """Show service status: pucit status oracle"""
59
+ _oracle_target(target)
60
+ from pucit.commands.oracle import oracle_status
61
+
62
+ oracle_status()
63
+
64
+
65
+ @app.command("logs")
66
+ def logs_cmd(
67
+ target: str = typer.Argument("oracle"),
68
+ follow: bool = typer.Option(False, "--follow", "-f"),
69
+ tail: int = typer.Option(100, "--tail"),
70
+ ) -> None:
71
+ """Show service logs: pucit logs oracle"""
72
+ _oracle_target(target)
73
+ from pucit.commands.oracle import oracle_logs
74
+
75
+ oracle_logs(follow=follow, tail=tail)
76
+
77
+
78
+ @app.command("run")
79
+ def run_cmd(
80
+ sources: Optional[List[str]] = typer.Argument(None, help="C++ sources (default: main.cpp)"),
81
+ flags: Optional[str] = typer.Option(None, "--flags", "-f", help='Extra flags e.g. "-O2"'),
82
+ std: Optional[str] = typer.Option(None, "--std", help="Language standard"),
83
+ out: Optional[str] = typer.Option(None, "--out", "-o", help="Output binary"),
84
+ input_file: Optional[str] = typer.Option(None, "--input", "-i", help="stdin from file"),
85
+ ) -> None:
86
+ """Compile and run: pucit run main.cpp"""
87
+ try:
88
+ code = cpp_build.execute_run(
89
+ list(sources or []),
90
+ flags=flags,
91
+ std=std,
92
+ out=out,
93
+ input_file=input_file,
94
+ )
95
+ except (FileNotFoundError, RuntimeError) as exc:
96
+ fail(str(exc))
97
+ raise typer.Exit(1) from exc
98
+ raise typer.Exit(code)
99
+
100
+
101
+ @app.command("compile")
102
+ def compile_cmd(
103
+ sources: Optional[List[str]] = typer.Argument(None),
104
+ flags: Optional[str] = typer.Option(None, "--flags", "-f"),
105
+ std: Optional[str] = typer.Option(None, "--std"),
106
+ out: Optional[str] = typer.Option(None, "--out", "-o"),
107
+ ) -> None:
108
+ """Compile only: pucit compile main.cpp"""
109
+ try:
110
+ code = cpp_build.execute_run(
111
+ list(sources or []),
112
+ flags=flags,
113
+ std=std,
114
+ out=out,
115
+ compile_only=True,
116
+ )
117
+ except (FileNotFoundError, RuntimeError) as exc:
118
+ fail(str(exc))
119
+ raise typer.Exit(1) from exc
120
+ raise typer.Exit(code)
121
+
122
+
123
+ @app.command("debug")
124
+ def debug_cmd(
125
+ sources: Optional[List[str]] = typer.Argument(None),
126
+ flags: Optional[str] = typer.Option(None, "--flags", "-f"),
127
+ std: Optional[str] = typer.Option(None, "--std"),
128
+ out: Optional[str] = typer.Option(None, "--out", "-o"),
129
+ ) -> None:
130
+ """Compile with -g and open gdb."""
131
+ try:
132
+ resolved = cpp_build.resolve_sources(list(sources or []))
133
+ code, binary, _ = cpp_build.compile_sources(
134
+ resolved, out=out, flags=flags, std=std, debug=True
135
+ )
136
+ except (FileNotFoundError, RuntimeError) as exc:
137
+ fail(str(exc))
138
+ raise typer.Exit(1) from exc
139
+ if code != 0:
140
+ raise typer.Exit(code)
141
+ gdb = which("gdb")
142
+ if not gdb:
143
+ fail("gdb not found. Run: pucit install pf")
144
+ raise typer.Exit(1)
145
+ info(f"Starting gdb on {binary}")
146
+ raise typer.Exit(run_streaming([gdb, str(binary)]))
147
+
148
+
149
+ @app.command("watch")
150
+ def watch_cmd(
151
+ sources: Optional[List[str]] = typer.Argument(None),
152
+ flags: Optional[str] = typer.Option(None, "--flags", "-f"),
153
+ std: Optional[str] = typer.Option(None, "--std"),
154
+ out: Optional[str] = typer.Option(None, "--out", "-o"),
155
+ input_file: Optional[str] = typer.Option(None, "--input", "-i"),
156
+ ) -> None:
157
+ """Recompile + run on file change."""
158
+ try:
159
+ code = cpp_build.watch_run(
160
+ list(sources or []),
161
+ flags=flags,
162
+ std=std,
163
+ out=out,
164
+ input_file=input_file,
165
+ )
166
+ except (FileNotFoundError, RuntimeError) as exc:
167
+ fail(str(exc))
168
+ raise typer.Exit(1) from exc
169
+ raise typer.Exit(code)
170
+
171
+
172
+ @app.command("clean")
173
+ def clean_cmd() -> None:
174
+ """Remove build/ and a.out in the current directory."""
175
+ removed = cpp_build.clean_artifacts()
176
+ if not removed:
177
+ info("Nothing to clean")
178
+ else:
179
+ for path in removed:
180
+ ok(f"Removed {path}")
181
+
182
+
183
+ @app.command("new")
184
+ def new_cmd(
185
+ name: str = typer.Argument(..., help="File or stem, e.g. hello or hello.cpp"),
186
+ force: bool = typer.Option(False, "--force", "-f"),
187
+ ) -> None:
188
+ """Create a new C++ file from template."""
189
+ new_file(name, force=force)
190
+
191
+
192
+ @app.command("init")
193
+ def init_cmd(force: bool = typer.Option(False, "--force", "-f")) -> None:
194
+ """Scaffold a PF lab folder (main.cpp, Makefile, .gitignore)."""
195
+ init_project(force=force)
196
+
197
+
198
+ @app.command("doctor")
199
+ def doctor_cmd() -> None:
200
+ """Check compilers, Docker, Oracle, bypass, and friends."""
201
+ raise typer.Exit(run_doctor())
202
+
203
+
204
+ @app.command("list")
205
+ def list_cmd() -> None:
206
+ """List managed components and their status."""
207
+ raise typer.Exit(run_list())
208
+
209
+
210
+ @app.command("version")
211
+ def version_cmd() -> None:
212
+ """Show pucit version."""
213
+ typer.echo(__version__)
214
+
215
+
216
+ @app.command("which")
217
+ def which_cmd(tool: str = typer.Argument(..., help="Tool name, e.g. g++ or docker")) -> None:
218
+ """Show resolved path for a tool."""
219
+ # allow g++ style
220
+ path = which(tool) or first_existing((tool,))
221
+ if not path and tool in {"g++", "cxx", "compiler"}:
222
+ path = cpp_build.find_compiler()
223
+ if not path and tool == "docker":
224
+ path = d.docker_bin()
225
+ if path:
226
+ typer.echo(path)
227
+ else:
228
+ fail(f"{tool} not found")
229
+ raise typer.Exit(1)
230
+
231
+
232
+ @app.command("open")
233
+ def open_cmd(path: str = typer.Argument(".", help="Path to open")) -> None:
234
+ """Open a folder/file in VS Code, Cursor, or the file manager."""
235
+ target = Path(path).resolve()
236
+ editors = ("cursor", "code", "codium")
237
+ for editor in editors:
238
+ binary = which(editor)
239
+ if binary:
240
+ raise typer.Exit(run_cmd([binary, str(target)]).returncode)
241
+ # fallback file manager / start
242
+ import sys
243
+
244
+ if sys.platform.startswith("win"):
245
+ raise typer.Exit(run_cmd(["explorer", str(target)]).returncode)
246
+ opener = which("xdg-open")
247
+ if opener:
248
+ raise typer.Exit(run_cmd([opener, str(target)]).returncode)
249
+ fail("No editor or file manager found")
250
+ raise typer.Exit(1)
251
+
252
+
253
+ def main() -> None:
254
+ app()
255
+
256
+
257
+ if __name__ == "__main__":
258
+ main()
@@ -0,0 +1 @@
1
+ """Command modules for the pucit CLI."""
@@ -0,0 +1,55 @@
1
+ """Campus proxy via bypass-pucit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import subprocess
7
+ from typing import Optional
8
+
9
+ import typer
10
+
11
+ from pucit import config as cfg
12
+ from pucit.util import fail, info, ok, which
13
+
14
+ bypass_app = typer.Typer(help="Campus internet bypass (wraps bypass-pucit).")
15
+
16
+
17
+ def _bypass_argv() -> list:
18
+ binary = which("bypass_pucit")
19
+ if binary:
20
+ return [binary]
21
+ # fallback: python -m bypass_pucit
22
+ return [shutil.which("python3") or shutil.which("python") or "python3", "-m", "bypass_pucit"]
23
+
24
+
25
+ @bypass_app.command("set")
26
+ def bypass_set(
27
+ proxy: Optional[str] = typer.Option(
28
+ None, "--proxy", "-p", help="Proxy URL (default from config / bypass-pucit)"
29
+ ),
30
+ ) -> None:
31
+ """Apply campus proxy settings via bypass_pucit."""
32
+ argv = _bypass_argv() + ["set"]
33
+ proxy_url = proxy or str(cfg.get("proxy"))
34
+ if proxy_url:
35
+ argv.extend(["--proxy", proxy_url])
36
+ info(" ".join(argv))
37
+ result = subprocess.run(argv)
38
+ if result.returncode == 0:
39
+ ok("Proxy applied")
40
+ else:
41
+ fail("bypass_pucit set failed")
42
+ raise typer.Exit(result.returncode)
43
+
44
+
45
+ @bypass_app.command("unset")
46
+ def bypass_unset() -> None:
47
+ """Remove campus proxy settings."""
48
+ argv = _bypass_argv() + ["unset"]
49
+ info(" ".join(argv))
50
+ result = subprocess.run(argv)
51
+ if result.returncode == 0:
52
+ ok("Proxy removed")
53
+ else:
54
+ fail("bypass_pucit unset failed")
55
+ raise typer.Exit(result.returncode)
@@ -0,0 +1,80 @@
1
+ """Environment doctor and component listing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import platform
6
+ import sys
7
+
8
+ import typer
9
+ from rich.table import Table
10
+
11
+ from pucit import __version__
12
+ from pucit import docker_util as d
13
+ from pucit.cpp_build import find_compiler
14
+ from pucit.util import console, which
15
+
16
+ doctor_app = typer.Typer(help="Health checks (also available as top-level pucit doctor).")
17
+
18
+
19
+ def _row(table: Table, name: str, ok: bool, detail: str) -> None:
20
+ mark = "[green]ok[/green]" if ok else "[red]missing[/red]"
21
+ table.add_row(name, mark, detail)
22
+
23
+
24
+ def run_doctor() -> int:
25
+ table = Table(title=f"pucit doctor (v{__version__})")
26
+ table.add_column("Check")
27
+ table.add_column("Status")
28
+ table.add_column("Detail")
29
+
30
+ _row(table, "OS", True, f"{platform.system()} {platform.release()} ({platform.machine()})")
31
+ _row(table, "Python", True, sys.version.split()[0])
32
+
33
+ compiler = find_compiler()
34
+ _row(table, "C++ compiler", bool(compiler), compiler or "run: pucit install pf")
35
+ _row(table, "make", bool(which("make")), which("make") or "—")
36
+ _row(table, "gdb", bool(which("gdb")), which("gdb") or "—")
37
+ _row(table, "cmake", bool(which("cmake")), which("cmake") or "—")
38
+
39
+ docker = d.docker_bin()
40
+ docker_ok = bool(docker) and d.docker_available()
41
+ detail = docker or "run: pucit install docker"
42
+ if docker and not d.docker_available():
43
+ detail = f"{docker} found but daemon not usable"
44
+ _row(table, "Docker", docker_ok, detail)
45
+
46
+ name = d.container_name()
47
+ if docker_ok and d.container_exists(name):
48
+ state = "running" if d.container_running(name) else "stopped"
49
+ _row(table, "Oracle container", True, f"{name} ({state})")
50
+ else:
51
+ _row(table, "Oracle container", False, "run: pucit install oracle")
52
+
53
+ bypass = which("bypass_pucit")
54
+ _row(table, "bypass_pucit", bool(bypass), bypass or "pip install bypass-pucit")
55
+
56
+ sqlplus = which("sqlplus")
57
+ _row(table, "sqlplus", bool(sqlplus), sqlplus or "optional: pucit install sqlclient")
58
+
59
+ console.print(table)
60
+ return 0
61
+
62
+
63
+ def run_list() -> int:
64
+ table = Table(title="pucit managed components")
65
+ table.add_column("Component")
66
+ table.add_column("Status")
67
+
68
+ compiler = find_compiler()
69
+ table.add_row("pf / C++", "ready" if compiler else "not installed")
70
+ docker_ok = bool(d.docker_bin()) and d.docker_available()
71
+ table.add_row("docker", "ready" if docker_ok else "not ready")
72
+ name = d.container_name()
73
+ if docker_ok and d.container_exists(name):
74
+ table.add_row("oracle", "running" if d.container_running(name) else "stopped")
75
+ else:
76
+ table.add_row("oracle", "not installed")
77
+ table.add_row("bypass_pucit", "ready" if which("bypass_pucit") else "not installed")
78
+ table.add_row("sqlplus", "ready" if which("sqlplus") else "not installed")
79
+ console.print(table)
80
+ return 0
@@ -0,0 +1,83 @@
1
+ """Install subcommands: oracle, pf, docker, sqlclient."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+
9
+ from pucit.commands.oracle import oracle_install
10
+ from pucit.platform import PackageError, docker_packages, install_packages, pf_packages
11
+ from pucit.util import detect_pkg_manager, fail, info, is_windows, ok, run_cmd, which
12
+
13
+ install_app = typer.Typer(help="Install toolchains and services.")
14
+
15
+
16
+ @install_app.command("oracle")
17
+ def install_oracle(
18
+ password: Optional[str] = typer.Option(None, "--password", "-p"),
19
+ pull_only: bool = typer.Option(False, "--pull-only"),
20
+ ) -> None:
21
+ """Install Oracle Free via Docker."""
22
+ oracle_install(password=password, pull_only=pull_only)
23
+
24
+
25
+ @install_app.command("pf")
26
+ def install_pf() -> None:
27
+ """Install Programming Fundamentals C++ essentials."""
28
+ try:
29
+ code = install_packages(pf_packages(), title="PF / C++ essentials")
30
+ except PackageError as exc:
31
+ fail(str(exc))
32
+ raise typer.Exit(1) from exc
33
+ if which("g++") or which("clang++"):
34
+ ok(f"Compiler: {which('g++') or which('clang++')}")
35
+ else:
36
+ info("Packages installed; open a new shell if g++ is still not on PATH.")
37
+ raise typer.Exit(code)
38
+
39
+
40
+ @install_app.command("docker")
41
+ def install_docker() -> None:
42
+ """Install Docker Engine / Docker Desktop."""
43
+ try:
44
+ code = install_packages(docker_packages(), title="Docker")
45
+ except PackageError as exc:
46
+ fail(str(exc))
47
+ raise typer.Exit(1) from exc
48
+ if is_windows():
49
+ info("Start Docker Desktop from the Start menu, then re-open your terminal.")
50
+ else:
51
+ if which("systemctl"):
52
+ run_cmd(["sudo", "systemctl", "enable", "--now", "docker"], capture=True)
53
+ info("If needed, add yourself to the docker group: sudo usermod -aG docker $USER")
54
+ raise typer.Exit(code)
55
+
56
+
57
+ @install_app.command("sqlclient")
58
+ def install_sqlclient() -> None:
59
+ """Install or guide Oracle Instant Client / sqlplus."""
60
+ if which("sqlplus"):
61
+ ok(f"sqlplus already present: {which('sqlplus')}")
62
+ return
63
+
64
+ if is_windows():
65
+ info("Windows: download Instant Client (Basic + SQL*Plus) from Oracle:")
66
+ info(" https://www.oracle.com/database/technologies/instant-client/downloads.html")
67
+ info("Add the unzipped folder to PATH, then reopen the terminal.")
68
+ raise typer.Exit(0)
69
+
70
+ manager = detect_pkg_manager()
71
+ pkgs = ["oracle-instantclient-sqlplus"] if manager == "dnf" else []
72
+ if pkgs:
73
+ try:
74
+ code = install_packages(pkgs, title="Oracle SQL*Plus")
75
+ raise typer.Exit(code)
76
+ except PackageError:
77
+ pass
78
+
79
+ info("sqlplus is not in your distro repos by default.")
80
+ info("Download Instant Client Basic + SQL*Plus RPMs/ZIPs from:")
81
+ info(" https://www.oracle.com/database/technologies/instant-client/linux-x86-64-downloads.html")
82
+ info("Then: sudo alien/rpm install or unzip and export LD_LIBRARY_PATH + PATH.")
83
+ raise typer.Exit(0)
@@ -0,0 +1,176 @@
1
+ """Oracle Database Free via Docker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Optional
7
+
8
+ import typer
9
+
10
+ from pucit import docker_util as d
11
+ from pucit.platform import PackageError, docker_packages, install_packages
12
+ from pucit.util import fail, info, ok, run_cmd, warn
13
+
14
+ oracle_app = typer.Typer(help="Manage the local Oracle Free container.")
15
+
16
+
17
+ @oracle_app.command("install")
18
+ def oracle_install(
19
+ password: Optional[str] = typer.Option(
20
+ None, "--password", "-p", help="ORACLE_PWD (generated if omitted)"
21
+ ),
22
+ pull_only: bool = typer.Option(False, "--pull-only", help="Only pull the image"),
23
+ ) -> None:
24
+ """Install (pull + create) Oracle Free Docker container."""
25
+ if not d.docker_bin():
26
+ warn("Docker not found — trying to install it first")
27
+ try:
28
+ code = install_packages(docker_packages(), title="docker")
29
+ except PackageError as exc:
30
+ fail(str(exc))
31
+ raise typer.Exit(1) from exc
32
+ if code != 0:
33
+ raise typer.Exit(code)
34
+
35
+ if not d.docker_available():
36
+ fail("Docker is installed but not usable. Start the Docker daemon/Desktop and retry.")
37
+ raise typer.Exit(1)
38
+
39
+ info(f"Pulling {d.oracle_image()} …")
40
+ success, output = d.pull_image()
41
+ if not success:
42
+ fail("Image pull failed.")
43
+ if "unauthorized" in output.lower() or "denied" in output.lower() or "login" in output.lower():
44
+ info("Oracle Container Registry may require login / license accept:")
45
+ info(" 1) Visit https://container-registry.oracle.com/ and accept the Database Free terms")
46
+ info(" 2) docker login container-registry.oracle.com")
47
+ info(" 3) pucit install oracle")
48
+ else:
49
+ print(output)
50
+ raise typer.Exit(1)
51
+ ok("Image ready")
52
+
53
+ if pull_only:
54
+ return
55
+
56
+ name = d.container_name()
57
+ if d.container_exists(name):
58
+ ok(f"Container '{name}' already exists. Use: pucit start oracle")
59
+ d.print_connect_info()
60
+ return
61
+
62
+ pwd = password or os.environ.get("PUCIT_ORACLE_PWD") or d.load_oracle_password() or d.generate_password()
63
+ d.save_oracle_password(pwd)
64
+ argv = d.build_run_argv(pwd, name)
65
+ display = [a.replace(f"ORACLE_PWD={pwd}", "ORACLE_PWD=***") for a in argv]
66
+ info(" ".join(display))
67
+ result = run_cmd(argv, capture=True)
68
+ if result.returncode != 0:
69
+ fail((result.stderr or result.stdout or "docker run failed").strip())
70
+ raise typer.Exit(result.returncode)
71
+ ok(f"Created container '{name}'")
72
+ info("Oracle may take 1–2 minutes to become ready. Check: pucit status oracle")
73
+ d.print_connect_info(pwd)
74
+
75
+
76
+ @oracle_app.command("start")
77
+ def oracle_start() -> None:
78
+ """Start the Oracle container."""
79
+ name = d.container_name()
80
+ if not d.container_exists(name):
81
+ fail(f"Container '{name}' not found. Run: pucit install oracle")
82
+ raise typer.Exit(1)
83
+ if d.container_running(name):
84
+ ok(f"'{name}' is already running")
85
+ return
86
+ result = run_cmd(d.docker_argv("start", name), capture=True)
87
+ if result.returncode != 0:
88
+ fail((result.stderr or "start failed").strip())
89
+ raise typer.Exit(result.returncode)
90
+ ok(f"Started '{name}'")
91
+
92
+
93
+ @oracle_app.command("stop")
94
+ def oracle_stop() -> None:
95
+ """Stop the Oracle container."""
96
+ name = d.container_name()
97
+ if not d.container_exists(name):
98
+ fail(f"Container '{name}' not found")
99
+ raise typer.Exit(1)
100
+ result = run_cmd(d.docker_argv("stop", name), capture=True)
101
+ if result.returncode != 0:
102
+ fail((result.stderr or "stop failed").strip())
103
+ raise typer.Exit(result.returncode)
104
+ ok(f"Stopped '{name}'")
105
+
106
+
107
+ @oracle_app.command("status")
108
+ def oracle_status() -> None:
109
+ """Show Oracle container status."""
110
+ name = d.container_name()
111
+ if not d.docker_bin():
112
+ fail("Docker/Podman not found")
113
+ raise typer.Exit(1)
114
+ if not d.container_exists(name):
115
+ warn(f"Container '{name}' is not installed")
116
+ raise typer.Exit(1)
117
+ state = "running" if d.container_running(name) else "stopped"
118
+ ok(f"{name}: {state}")
119
+ d.print_connect_info()
120
+
121
+
122
+ @oracle_app.command("logs")
123
+ def oracle_logs(
124
+ follow: bool = typer.Option(False, "--follow", "-f", help="Follow log output"),
125
+ tail: int = typer.Option(100, "--tail", help="Lines to show"),
126
+ ) -> None:
127
+ """Show Oracle container logs."""
128
+ name = d.container_name()
129
+ argv = d.docker_argv("logs", "--tail", str(tail))
130
+ if follow:
131
+ argv.append("-f")
132
+ argv.append(name)
133
+ raise typer.Exit(run_cmd(argv).returncode)
134
+
135
+
136
+ @oracle_app.command("rm")
137
+ def oracle_rm(
138
+ volumes: bool = typer.Option(False, "--volumes", "-v", help="Also remove data volume"),
139
+ force: bool = typer.Option(False, "--force", "-f", help="Force remove"),
140
+ ) -> None:
141
+ """Remove the Oracle container."""
142
+ name = d.container_name()
143
+ if not d.container_exists(name):
144
+ warn(f"Container '{name}' not found")
145
+ return
146
+ argv = d.docker_argv("rm")
147
+ if force:
148
+ argv.append("-f")
149
+ if volumes:
150
+ argv.append("-v")
151
+ argv.append(name)
152
+ result = run_cmd(argv, capture=True)
153
+ if result.returncode != 0:
154
+ fail((result.stderr or "rm failed").strip())
155
+ raise typer.Exit(result.returncode)
156
+ ok(f"Removed '{name}'")
157
+
158
+
159
+ @oracle_app.command("connect")
160
+ def oracle_connect(
161
+ launch: bool = typer.Option(False, "--launch", "-l", help="Launch sqlplus if available"),
162
+ ) -> None:
163
+ """Print (or launch) a sqlplus connection."""
164
+ from pucit.util import which
165
+
166
+ d.print_connect_info()
167
+ if not launch:
168
+ return
169
+ sqlplus = which("sqlplus")
170
+ if not sqlplus:
171
+ fail("sqlplus not found. Try: pucit install sqlclient")
172
+ raise typer.Exit(1)
173
+ pwd = d.load_oracle_password() or "oracle"
174
+ port = d.oracle_port()
175
+ conn = f"system/{pwd}@//localhost:{port}/FREEPDB1"
176
+ raise typer.Exit(run_cmd([sqlplus, conn]).returncode)