borescope 0.1.0.dev0__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.
@@ -0,0 +1,81 @@
1
+ """Command base class, result type, and the auto-discovery registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, ClassVar
7
+
8
+ if TYPE_CHECKING:
9
+ from ..context import ShellContext
10
+
11
+
12
+ @dataclass
13
+ class Result:
14
+ """The outcome of running a command stage."""
15
+
16
+ output: str = ""
17
+ error: str = ""
18
+ code: int = 0
19
+
20
+ @classmethod
21
+ def ok(cls, output: str = "") -> Result:
22
+ return cls(output=output, code=0)
23
+
24
+ @classmethod
25
+ def fail(cls, error: str, code: int = 1) -> Result:
26
+ return cls(error=error, code=code)
27
+
28
+
29
+ class ExitShell(Exception): # noqa: N818 - control-flow signal, not an error
30
+ """Raised by ``exit`` to leave the REPL cleanly."""
31
+
32
+ def __init__(self, code: int = 0):
33
+ super().__init__(code)
34
+ self.code = code
35
+
36
+
37
+ class Command:
38
+ """Base class for all built-in commands.
39
+
40
+ Subclasses set ``name`` (and optionally ``aliases``) and implement
41
+ :meth:`run`. They are auto-discovered by :func:`build_registry`, so adding a
42
+ command is just defining a subclass — no registration boilerplate.
43
+ """
44
+
45
+ name: ClassVar[str] = ""
46
+ aliases: ClassVar[tuple[str, ...]] = ()
47
+ summary: ClassVar[str] = ""
48
+ usage: ClassVar[str] = ""
49
+ # Streaming commands write directly to the terminal and cannot appear in a
50
+ # pipe (e.g. `logs --follow`, `tail -f`).
51
+ streaming: ClassVar[bool] = False
52
+
53
+ def run(
54
+ self, ctx: ShellContext, args: list[str], stdin: str | None = None
55
+ ) -> Result:
56
+ raise NotImplementedError
57
+
58
+
59
+ def _iter_command_classes(root: type[Command]) -> list[type[Command]]:
60
+ found: list[type[Command]] = []
61
+ for sub in root.__subclasses__():
62
+ found.extend(_iter_command_classes(sub))
63
+ if getattr(sub, "name", ""):
64
+ found.append(sub)
65
+ return found
66
+
67
+
68
+ def build_registry() -> dict[str, Command]:
69
+ """Instantiate every discovered command, keyed by name and alias."""
70
+ # Importing the package registers all Command subclasses.
71
+ from . import import_all
72
+
73
+ import_all()
74
+
75
+ registry: dict[str, Command] = {}
76
+ for klass in _iter_command_classes(Command):
77
+ instance = klass()
78
+ registry[klass.name] = instance
79
+ for alias in klass.aliases:
80
+ registry[alias] = instance
81
+ return registry
@@ -0,0 +1,117 @@
1
+ """Shell-state and trivial commands: cd, pwd, echo, env, exit, clear, help."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from .. import pathutils
8
+ from .base import Command, ExitShell, Result
9
+
10
+ if TYPE_CHECKING:
11
+ from ..context import ShellContext
12
+
13
+
14
+ class Cd(Command):
15
+ name = "cd"
16
+ summary = "Change the current directory"
17
+ usage = "cd [dir]"
18
+
19
+ def run(
20
+ self, ctx: ShellContext, args: list[str], stdin: str | None = None
21
+ ) -> Result:
22
+ target = args[0] if args else "~"
23
+ path = pathutils.resolve(ctx.cwd, target, home=ctx.home)
24
+ try:
25
+ infos = ctx.transport.list_files(path, itself=True)
26
+ except Exception as exc: # noqa: BLE001
27
+ return Result.fail(f"cd: {target}: {exc}")
28
+ if infos and getattr(infos[0].type, "name", "") != "DIRECTORY":
29
+ return Result.fail(f"cd: not a directory: {target}")
30
+ ctx.chdir(path)
31
+ return Result()
32
+
33
+
34
+ class Pwd(Command):
35
+ name = "pwd"
36
+ summary = "Print the current directory"
37
+
38
+ def run(
39
+ self, ctx: ShellContext, args: list[str], stdin: str | None = None
40
+ ) -> Result:
41
+ return Result.ok(ctx.cwd)
42
+
43
+
44
+ class Echo(Command):
45
+ name = "echo"
46
+ summary = "Write arguments to output"
47
+ usage = "echo [args...]"
48
+
49
+ def run(
50
+ self, ctx: ShellContext, args: list[str], stdin: str | None = None
51
+ ) -> Result:
52
+ return Result.ok(" ".join(args))
53
+
54
+
55
+ class Env(Command):
56
+ name = "env"
57
+ summary = "Show the shell's tracked environment"
58
+
59
+ def run(
60
+ self, ctx: ShellContext, args: list[str], stdin: str | None = None
61
+ ) -> Result:
62
+ lines = [f"{key}={value}" for key, value in sorted(ctx.env.items())]
63
+ return Result.ok("\n".join(lines))
64
+
65
+
66
+ class Exit(Command):
67
+ name = "exit"
68
+ aliases = ("quit",)
69
+ summary = "Leave borescope"
70
+ usage = "exit [code]"
71
+
72
+ def run(
73
+ self, ctx: ShellContext, args: list[str], stdin: str | None = None
74
+ ) -> Result:
75
+ code = 0
76
+ if args:
77
+ try:
78
+ code = int(args[0])
79
+ except ValueError:
80
+ code = 1
81
+ raise ExitShell(code)
82
+
83
+
84
+ class Clear(Command):
85
+ name = "clear"
86
+ summary = "Clear the screen"
87
+
88
+ def run(
89
+ self, ctx: ShellContext, args: list[str], stdin: str | None = None
90
+ ) -> Result:
91
+ return Result.ok("\033[H\033[2J")
92
+
93
+
94
+ class Help(Command):
95
+ name = "help"
96
+ aliases = ("?",)
97
+ summary = "List available commands"
98
+
99
+ def run(
100
+ self, ctx: ShellContext, args: list[str], stdin: str | None = None
101
+ ) -> Result:
102
+ from .base import build_registry
103
+
104
+ seen: dict[str, Command] = {}
105
+ for name, cmd in build_registry().items():
106
+ if cmd.name == name: # skip aliases
107
+ seen[name] = cmd
108
+ width = max((len(n) for n in seen), default=0)
109
+ lines = [
110
+ f" {name.ljust(width)} {cmd.summary}"
111
+ for name, cmd in sorted(seen.items())
112
+ ]
113
+ header = (
114
+ "Built-in commands (anything else: 'exec <cmd> ...' runs it in the "
115
+ "container):"
116
+ )
117
+ return Result.ok(header + "\n" + "\n".join(lines))
@@ -0,0 +1,36 @@
1
+ """The ``exec`` escape hatch — run any binary that's already in the container."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from .base import Command, Result
8
+
9
+ if TYPE_CHECKING:
10
+ from ..context import ShellContext
11
+
12
+
13
+ class Exec(Command):
14
+ name = "exec"
15
+ summary = "Run a program inside the container (escape hatch)"
16
+ usage = "exec <command> [args...]"
17
+
18
+ def run(
19
+ self, ctx: ShellContext, args: list[str], stdin: str | None = None
20
+ ) -> Result:
21
+ if not args:
22
+ return Result.fail("exec: usage: exec <command> [args...]")
23
+ from ops import pebble
24
+
25
+ try:
26
+ process = ctx.transport.exec(args, working_dir=ctx.cwd, stdin=stdin)
27
+ out, err = process.wait_output()
28
+ except pebble.ExecError as exc:
29
+ return Result(
30
+ output=exc.stdout or "",
31
+ error=exc.stderr or "",
32
+ code=exc.exit_code or 1,
33
+ )
34
+ except Exception as exc: # noqa: BLE001
35
+ return Result.fail(f"exec: {exc}")
36
+ return Result(output=out or "", error=err or "", code=0)