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.
- borescope/__init__.py +10 -0
- borescope/__main__.py +10 -0
- borescope/cli.py +144 -0
- borescope/discovery.py +256 -0
- borescope/errors.py +26 -0
- borescope/juju.py +89 -0
- borescope/shell/__init__.py +8 -0
- borescope/shell/commands/__init__.py +13 -0
- borescope/shell/commands/_args.py +54 -0
- borescope/shell/commands/base.py +81 -0
- borescope/shell/commands/basic.py +117 -0
- borescope/shell/commands/execcmd.py +36 -0
- borescope/shell/commands/filesystem.py +494 -0
- borescope/shell/commands/pebble.py +388 -0
- borescope/shell/completion.py +77 -0
- borescope/shell/context.py +33 -0
- borescope/shell/history.py +29 -0
- borescope/shell/parser.py +91 -0
- borescope/shell/pathutils.py +21 -0
- borescope/shell/repl.py +133 -0
- borescope/shell/theme.py +35 -0
- borescope/snapshot.py +103 -0
- borescope/transport/__init__.py +162 -0
- borescope/transport/cli_transport.py +63 -0
- borescope/transport/relay.py +77 -0
- borescope/transport/runner.py +149 -0
- borescope/transport/socket_transport.py +29 -0
- borescope-0.1.0.dev0.dist-info/METADATA +100 -0
- borescope-0.1.0.dev0.dist-info/RECORD +32 -0
- borescope-0.1.0.dev0.dist-info/WHEEL +4 -0
- borescope-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- borescope-0.1.0.dev0.dist-info/licenses/LICENSE +203 -0
|
@@ -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)
|