swisscli 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.
- swisscli/__init__.py +5 -0
- swisscli/__main__.py +4 -0
- swisscli/cli.py +74 -0
- swisscli/loader.py +30 -0
- swisscli/plugin.py +160 -0
- swisscli/plugins/__init__.py +0 -0
- swisscli/plugins/package/__init__.py +0 -0
- swisscli/plugins/package/apt.py +47 -0
- swisscli/plugins/package/dnf.py +43 -0
- swisscli/plugins/package/dpkg.py +35 -0
- swisscli/plugins/package/rpm.py +35 -0
- swisscli/plugins/package/yum.py +43 -0
- swisscli/plugins/ssl/__init__.py +0 -0
- swisscli/plugins/ssl/openssl.py +37 -0
- swisscli/plugins/ssl/ssh_keygen.py +31 -0
- swisscli/plugins/web/__init__.py +0 -0
- swisscli/plugins/web/curl.py +21 -0
- swisscli/plugins/web/wget.py +19 -0
- swisscli-0.1.0.dist-info/METADATA +106 -0
- swisscli-0.1.0.dist-info/RECORD +22 -0
- swisscli-0.1.0.dist-info/WHEEL +4 -0
- swisscli-0.1.0.dist-info/entry_points.txt +2 -0
swisscli/__init__.py
ADDED
swisscli/__main__.py
ADDED
swisscli/cli.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from . import __version__
|
|
6
|
+
from .loader import discover_plugins
|
|
7
|
+
from .plugin import PluginRegistry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command(context_settings=dict(
|
|
11
|
+
help_option_names=[],
|
|
12
|
+
ignore_unknown_options=True,
|
|
13
|
+
))
|
|
14
|
+
@click.argument("args", nargs=-1)
|
|
15
|
+
def main(args):
|
|
16
|
+
"""swisscli - CLI wrapper ecosystem."""
|
|
17
|
+
|
|
18
|
+
discover_plugins()
|
|
19
|
+
|
|
20
|
+
if not args:
|
|
21
|
+
click.echo("Usage: swisscli <tool> [ARGS]...")
|
|
22
|
+
click.echo(" swisscli --help")
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
|
|
25
|
+
if args[0] in ("--help", "-h"):
|
|
26
|
+
_show_help()
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
if args[0] in ("--version", "-V"):
|
|
30
|
+
click.echo(f"swisscli version {__version__}")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
tool = args[0]
|
|
34
|
+
tool_args = list(args[1:])
|
|
35
|
+
|
|
36
|
+
if tool_args and tool_args[0] in ("--help", "-h"):
|
|
37
|
+
plugin = PluginRegistry.get(tool)
|
|
38
|
+
if plugin:
|
|
39
|
+
click.echo(plugin.format_help())
|
|
40
|
+
else:
|
|
41
|
+
click.echo(f"Unknown tool: {tool}", err=True)
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
plugin = PluginRegistry.get(tool)
|
|
45
|
+
if plugin is None:
|
|
46
|
+
click.echo(f"Error: unknown tool '{tool}'", err=True)
|
|
47
|
+
click.echo("Run 'swisscli --help' for available tools.")
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
if tool_args and tool_args[0] in plugin.subcommands:
|
|
51
|
+
sub_name = tool_args[0]
|
|
52
|
+
sub_args = tool_args[1:]
|
|
53
|
+
if sub_args and sub_args[0] in ("--help", "-h"):
|
|
54
|
+
click.echo(plugin.format_subcommand_help(plugin.subcommands[sub_name]))
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
sys.exit(plugin.execute(tool_args))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _show_help():
|
|
61
|
+
by_category = PluginRegistry.by_category()
|
|
62
|
+
|
|
63
|
+
click.echo("Usage: swisscli <tool> [ARGS]...")
|
|
64
|
+
click.echo()
|
|
65
|
+
click.echo("Available tools:")
|
|
66
|
+
click.echo()
|
|
67
|
+
for category in sorted(by_category):
|
|
68
|
+
tools = by_category[category]
|
|
69
|
+
click.echo(f" {category}:")
|
|
70
|
+
for name in sorted(tools):
|
|
71
|
+
plugin = tools[name]
|
|
72
|
+
click.echo(f" {name:<12} {plugin.description}")
|
|
73
|
+
click.echo()
|
|
74
|
+
click.echo("Run 'swisscli <tool> --help' for tool-specific help.")
|
swisscli/loader.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import pkgutil
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
from .plugin import PluginRegistry
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _import_module(modname: str):
|
|
9
|
+
try:
|
|
10
|
+
return importlib.import_module(modname)
|
|
11
|
+
except Exception as exc:
|
|
12
|
+
warnings.warn(f"Failed to load plugin module '{modname}': {exc}")
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def discover_plugins() -> None:
|
|
17
|
+
import swisscli.plugins as plugins_pkg
|
|
18
|
+
|
|
19
|
+
prefix = plugins_pkg.__name__ + "."
|
|
20
|
+
seen = set()
|
|
21
|
+
|
|
22
|
+
for importer, modname, ispkg in pkgutil.walk_packages(
|
|
23
|
+
plugins_pkg.__path__, prefix=prefix
|
|
24
|
+
):
|
|
25
|
+
if ispkg or modname in seen:
|
|
26
|
+
continue
|
|
27
|
+
seen.add(modname)
|
|
28
|
+
mod = _import_module(modname)
|
|
29
|
+
if mod is not None and hasattr(mod, "plugin"):
|
|
30
|
+
PluginRegistry.register(mod.plugin)
|
swisscli/plugin.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ArgMapper:
|
|
13
|
+
def __init__(self, mapping: Dict[str, str]):
|
|
14
|
+
self._mapping = mapping
|
|
15
|
+
|
|
16
|
+
def translate(self, args: List[str]) -> List[str]:
|
|
17
|
+
result = []
|
|
18
|
+
for arg in args:
|
|
19
|
+
result.append(self._mapping.get(arg, arg))
|
|
20
|
+
return result
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Subcommand:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
name: str,
|
|
27
|
+
description: str,
|
|
28
|
+
prefix_args: Optional[List[str]] = None,
|
|
29
|
+
suffix_args: Optional[List[str]] = None,
|
|
30
|
+
arg_mapper: Optional[ArgMapper] = None,
|
|
31
|
+
):
|
|
32
|
+
self.name = name
|
|
33
|
+
self.description = description
|
|
34
|
+
self.prefix_args = prefix_args or []
|
|
35
|
+
self.suffix_args = suffix_args or []
|
|
36
|
+
self.arg_mapper = arg_mapper
|
|
37
|
+
|
|
38
|
+
def translate_args(self, args: List[str]) -> List[str]:
|
|
39
|
+
if self.arg_mapper:
|
|
40
|
+
return self.arg_mapper.translate(args)
|
|
41
|
+
return list(args)
|
|
42
|
+
|
|
43
|
+
def build_command(self, tool_name: str, args: List[str]) -> List[str]:
|
|
44
|
+
return [tool_name, *self.prefix_args, *self.translate_args(args), *self.suffix_args]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Plugin(ABC):
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def name(self) -> str:
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def description(self) -> str:
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def category(self) -> str:
|
|
61
|
+
return "general"
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def arg_mapper(self) -> Optional[ArgMapper]:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def subcommands(self) -> Dict[str, Subcommand]:
|
|
69
|
+
return {}
|
|
70
|
+
|
|
71
|
+
def translate_args(self, args: List[str]) -> List[str]:
|
|
72
|
+
if self.arg_mapper:
|
|
73
|
+
return self.arg_mapper.translate(args)
|
|
74
|
+
return list(args)
|
|
75
|
+
|
|
76
|
+
def build_command(self, args: List[str]) -> List[str]:
|
|
77
|
+
return [self.name, *self.translate_args(args)]
|
|
78
|
+
|
|
79
|
+
def _is_available(self) -> bool:
|
|
80
|
+
return shutil.which(self.name) is not None
|
|
81
|
+
|
|
82
|
+
def _get_fallback(self) -> Optional[Plugin]:
|
|
83
|
+
for plugin in PluginRegistry.all().values():
|
|
84
|
+
if plugin is not self and plugin.category == self.category and plugin._is_available():
|
|
85
|
+
return plugin
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def execute(self, args: List[str]) -> int:
|
|
89
|
+
if not self._is_available():
|
|
90
|
+
fallback = self._get_fallback()
|
|
91
|
+
if fallback is not None:
|
|
92
|
+
click.echo(
|
|
93
|
+
f"'{self.name}' not found, falling back to '{fallback.name}'",
|
|
94
|
+
err=True,
|
|
95
|
+
)
|
|
96
|
+
return fallback.execute(args)
|
|
97
|
+
click.echo(
|
|
98
|
+
f"Error: '{self.name}' not found and no fallback available in category '{self.category}'.",
|
|
99
|
+
err=True,
|
|
100
|
+
)
|
|
101
|
+
return 1
|
|
102
|
+
|
|
103
|
+
if args and args[0] in self.subcommands:
|
|
104
|
+
sub = self.subcommands[args[0]]
|
|
105
|
+
cmd = sub.build_command(self.name, args[1:])
|
|
106
|
+
return subprocess.call(cmd)
|
|
107
|
+
|
|
108
|
+
cmd = self.build_command(args)
|
|
109
|
+
return subprocess.call(cmd)
|
|
110
|
+
|
|
111
|
+
def format_help(self) -> str:
|
|
112
|
+
lines = [f"{self.name}: {self.description}", ""]
|
|
113
|
+
if self.subcommands:
|
|
114
|
+
lines.append("Subcommands:")
|
|
115
|
+
for name, sub in self.subcommands.items():
|
|
116
|
+
lines.append(f" {name:<12} {sub.description}")
|
|
117
|
+
lines.append("")
|
|
118
|
+
if self.arg_mapper and self.arg_mapper._mapping:
|
|
119
|
+
lines.append("Argument mappings:")
|
|
120
|
+
for alias, native in self.arg_mapper._mapping.items():
|
|
121
|
+
lines.append(f" {alias} \u2192 {native}")
|
|
122
|
+
lines.append("")
|
|
123
|
+
lines.append(f"Category: {self.category}")
|
|
124
|
+
return "\n".join(lines)
|
|
125
|
+
|
|
126
|
+
def format_subcommand_help(self, sub: Subcommand) -> str:
|
|
127
|
+
lines = [f"{self.name} {sub.name}: {sub.description}", ""]
|
|
128
|
+
if sub.arg_mapper and sub.arg_mapper._mapping:
|
|
129
|
+
lines.append("Argument mappings:")
|
|
130
|
+
for alias, native in sub.arg_mapper._mapping.items():
|
|
131
|
+
lines.append(f" {alias} \u2192 {native}")
|
|
132
|
+
lines.append("")
|
|
133
|
+
lines.append(f"Runs: {' '.join(sub.build_command(self.name, ['<args>']))}")
|
|
134
|
+
return "\n".join(lines)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class PluginRegistry:
|
|
138
|
+
_plugins: Dict[str, Plugin] = {}
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def register(cls, plugin: Plugin) -> None:
|
|
142
|
+
cls._plugins[plugin.name] = plugin
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def get(cls, name: str) -> Optional[Plugin]:
|
|
146
|
+
return cls._plugins.get(name)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def all(cls) -> Dict[str, Plugin]:
|
|
150
|
+
return dict(cls._plugins)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def by_category(cls) -> Dict[str, Dict[str, Plugin]]:
|
|
154
|
+
result: Dict[str, Dict[str, Plugin]] = {}
|
|
155
|
+
for name, plugin in cls._plugins.items():
|
|
156
|
+
cat = plugin.category
|
|
157
|
+
if cat not in result:
|
|
158
|
+
result[cat] = {}
|
|
159
|
+
result[cat][name] = plugin
|
|
160
|
+
return result
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from swisscli.plugin import ArgMapper, Plugin, Subcommand
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AptPlugin(Plugin):
|
|
5
|
+
name = "apt"
|
|
6
|
+
description = "Debian/Ubuntu package manager"
|
|
7
|
+
category = "package"
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def subcommands(self):
|
|
11
|
+
return {
|
|
12
|
+
"install": Subcommand(
|
|
13
|
+
name="install",
|
|
14
|
+
description="Install packages",
|
|
15
|
+
prefix_args=["install"],
|
|
16
|
+
arg_mapper=ArgMapper({
|
|
17
|
+
"--yes": "-y",
|
|
18
|
+
"--assume-yes": "-y",
|
|
19
|
+
"--no-install-recommends": "--no-install-recommends",
|
|
20
|
+
"--reinstall": "--reinstall",
|
|
21
|
+
}),
|
|
22
|
+
),
|
|
23
|
+
"remove": Subcommand(
|
|
24
|
+
name="remove",
|
|
25
|
+
description="Remove packages",
|
|
26
|
+
prefix_args=["remove"],
|
|
27
|
+
arg_mapper=ArgMapper({
|
|
28
|
+
"--yes": "-y",
|
|
29
|
+
"--assume-yes": "-y",
|
|
30
|
+
"--purge": "--purge",
|
|
31
|
+
"--autoremove": "--autoremove",
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
34
|
+
"search": Subcommand(
|
|
35
|
+
name="search",
|
|
36
|
+
description="Search for packages",
|
|
37
|
+
prefix_args=["search"],
|
|
38
|
+
),
|
|
39
|
+
"list": Subcommand(
|
|
40
|
+
name="list",
|
|
41
|
+
description="List installed packages",
|
|
42
|
+
prefix_args=["list", "--installed"],
|
|
43
|
+
),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
plugin = AptPlugin()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from swisscli.plugin import ArgMapper, Plugin, Subcommand
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DnfPlugin(Plugin):
|
|
5
|
+
name = "dnf"
|
|
6
|
+
description = "RPM package manager (modern)"
|
|
7
|
+
category = "package"
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def subcommands(self):
|
|
11
|
+
return {
|
|
12
|
+
"install": Subcommand(
|
|
13
|
+
name="install",
|
|
14
|
+
description="Install packages",
|
|
15
|
+
prefix_args=["install"],
|
|
16
|
+
arg_mapper=ArgMapper({
|
|
17
|
+
"--yes": "-y",
|
|
18
|
+
"--assumeyes": "-y",
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
"remove": Subcommand(
|
|
22
|
+
name="remove",
|
|
23
|
+
description="Remove packages",
|
|
24
|
+
prefix_args=["remove"],
|
|
25
|
+
arg_mapper=ArgMapper({
|
|
26
|
+
"--yes": "-y",
|
|
27
|
+
"--assumeyes": "-y",
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
"search": Subcommand(
|
|
31
|
+
name="search",
|
|
32
|
+
description="Search for packages",
|
|
33
|
+
prefix_args=["search"],
|
|
34
|
+
),
|
|
35
|
+
"list": Subcommand(
|
|
36
|
+
name="list",
|
|
37
|
+
description="List installed packages",
|
|
38
|
+
prefix_args=["list", "--installed"],
|
|
39
|
+
),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
plugin = DnfPlugin()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from swisscli.plugin import Plugin, Subcommand
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DpkgPlugin(Plugin):
|
|
5
|
+
name = "dpkg"
|
|
6
|
+
description = "Debian package manager (low-level)"
|
|
7
|
+
category = "package"
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def subcommands(self):
|
|
11
|
+
return {
|
|
12
|
+
"list": Subcommand(
|
|
13
|
+
name="list",
|
|
14
|
+
description="List installed packages",
|
|
15
|
+
prefix_args=["-l"],
|
|
16
|
+
),
|
|
17
|
+
"info": Subcommand(
|
|
18
|
+
name="info",
|
|
19
|
+
description="Show package info",
|
|
20
|
+
prefix_args=["-s"],
|
|
21
|
+
),
|
|
22
|
+
"search": Subcommand(
|
|
23
|
+
name="search",
|
|
24
|
+
description="Search which package owns a file",
|
|
25
|
+
prefix_args=["-S"],
|
|
26
|
+
),
|
|
27
|
+
"contents": Subcommand(
|
|
28
|
+
name="contents",
|
|
29
|
+
description="List files installed by a package",
|
|
30
|
+
prefix_args=["-L"],
|
|
31
|
+
),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
plugin = DpkgPlugin()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from swisscli.plugin import Plugin, Subcommand
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RpmPlugin(Plugin):
|
|
5
|
+
name = "rpm"
|
|
6
|
+
description = "RPM package manager (low-level)"
|
|
7
|
+
category = "package"
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def subcommands(self):
|
|
11
|
+
return {
|
|
12
|
+
"list": Subcommand(
|
|
13
|
+
name="list",
|
|
14
|
+
description="List installed packages",
|
|
15
|
+
prefix_args=["-qa"],
|
|
16
|
+
),
|
|
17
|
+
"info": Subcommand(
|
|
18
|
+
name="info",
|
|
19
|
+
description="Show package info",
|
|
20
|
+
prefix_args=["-qi"],
|
|
21
|
+
),
|
|
22
|
+
"search": Subcommand(
|
|
23
|
+
name="search",
|
|
24
|
+
description="Search installed packages",
|
|
25
|
+
prefix_args=["-q"],
|
|
26
|
+
),
|
|
27
|
+
"files": Subcommand(
|
|
28
|
+
name="files",
|
|
29
|
+
description="List files in a package",
|
|
30
|
+
prefix_args=["-ql"],
|
|
31
|
+
),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
plugin = RpmPlugin()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from swisscli.plugin import ArgMapper, Plugin, Subcommand
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class YumPlugin(Plugin):
|
|
5
|
+
name = "yum"
|
|
6
|
+
description = "RPM package manager (legacy)"
|
|
7
|
+
category = "package"
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def subcommands(self):
|
|
11
|
+
return {
|
|
12
|
+
"install": Subcommand(
|
|
13
|
+
name="install",
|
|
14
|
+
description="Install packages",
|
|
15
|
+
prefix_args=["install"],
|
|
16
|
+
arg_mapper=ArgMapper({
|
|
17
|
+
"--yes": "-y",
|
|
18
|
+
"--assumeyes": "-y",
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
"remove": Subcommand(
|
|
22
|
+
name="remove",
|
|
23
|
+
description="Remove packages",
|
|
24
|
+
prefix_args=["remove"],
|
|
25
|
+
arg_mapper=ArgMapper({
|
|
26
|
+
"--yes": "-y",
|
|
27
|
+
"--assumeyes": "-y",
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
"search": Subcommand(
|
|
31
|
+
name="search",
|
|
32
|
+
description="Search for packages",
|
|
33
|
+
prefix_args=["search"],
|
|
34
|
+
),
|
|
35
|
+
"list": Subcommand(
|
|
36
|
+
name="list",
|
|
37
|
+
description="List installed packages",
|
|
38
|
+
prefix_args=["list", "installed"],
|
|
39
|
+
),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
plugin = YumPlugin()
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from swisscli.plugin import ArgMapper, Plugin, Subcommand
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class OpenSslPlugin(Plugin):
|
|
5
|
+
name = "openssl"
|
|
6
|
+
description = "OpenSSL certificate toolkit"
|
|
7
|
+
category = "ssl"
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def subcommands(self):
|
|
11
|
+
return {
|
|
12
|
+
"show": Subcommand(
|
|
13
|
+
name="show",
|
|
14
|
+
description="Show certificate details",
|
|
15
|
+
prefix_args=["x509", "-in"],
|
|
16
|
+
suffix_args=["-text", "-noout"],
|
|
17
|
+
),
|
|
18
|
+
"generate": Subcommand(
|
|
19
|
+
name="generate",
|
|
20
|
+
description="Generate a self-signed certificate",
|
|
21
|
+
prefix_args=[
|
|
22
|
+
"req", "-x509", "-newkey", "rsa:4096",
|
|
23
|
+
"-keyout", "key.pem", "-out", "cert.pem",
|
|
24
|
+
"-days", "365", "-nodes",
|
|
25
|
+
"-subj", "/CN=SelfSigned",
|
|
26
|
+
],
|
|
27
|
+
arg_mapper=ArgMapper({
|
|
28
|
+
"--days": "-days",
|
|
29
|
+
"--keyout": "-keyout",
|
|
30
|
+
"--out": "-out",
|
|
31
|
+
"--subj": "-subj",
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
plugin = OpenSslPlugin()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from swisscli.plugin import ArgMapper, Plugin, Subcommand
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SshKeygenPlugin(Plugin):
|
|
5
|
+
name = "ssh-keygen"
|
|
6
|
+
description = "SSH key management tool"
|
|
7
|
+
category = "ssl"
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def subcommands(self):
|
|
11
|
+
return {
|
|
12
|
+
"show": Subcommand(
|
|
13
|
+
name="show",
|
|
14
|
+
description="Show SSH key fingerprint",
|
|
15
|
+
prefix_args=["-l", "-f"],
|
|
16
|
+
),
|
|
17
|
+
"generate": Subcommand(
|
|
18
|
+
name="generate",
|
|
19
|
+
description="Generate an SSH key pair",
|
|
20
|
+
prefix_args=["-t", "rsa", "-b", "4096", "-f", "key", "-N", ""],
|
|
21
|
+
arg_mapper=ArgMapper({
|
|
22
|
+
"--type": "-t",
|
|
23
|
+
"--bits": "-b",
|
|
24
|
+
"--file": "-f",
|
|
25
|
+
"--comment": "-C",
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
plugin = SshKeygenPlugin()
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from swisscli.plugin import ArgMapper, Plugin
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CurlPlugin(Plugin):
|
|
5
|
+
name = "curl"
|
|
6
|
+
description = "Wrapper around curl for HTTP requests"
|
|
7
|
+
category = "web-cli"
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def arg_mapper(self):
|
|
11
|
+
return ArgMapper({
|
|
12
|
+
"--insecure": "-k",
|
|
13
|
+
"--silent": "-s",
|
|
14
|
+
"--location": "-L",
|
|
15
|
+
"--output": "-o",
|
|
16
|
+
"--header": "-H",
|
|
17
|
+
"--data": "-d",
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
plugin = CurlPlugin()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from swisscli.plugin import ArgMapper, Plugin
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class WgetPlugin(Plugin):
|
|
5
|
+
name = "wget"
|
|
6
|
+
description = "Wrapper around wget for downloading files"
|
|
7
|
+
category = "web-cli"
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def arg_mapper(self):
|
|
11
|
+
return ArgMapper({
|
|
12
|
+
"--insecure": "--no-check-certificate",
|
|
13
|
+
"--quiet": "-q",
|
|
14
|
+
"--output": "-O",
|
|
15
|
+
"--timeout": "-T",
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
plugin = WgetPlugin()
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: swisscli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A plugin-based CLI wrapper ecosystem for common command-line tools
|
|
5
|
+
Project-URL: Homepage, https://github.com/yourusername/swisscli
|
|
6
|
+
Author-email: Your Name <you@example.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: click>=8.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# swisscli
|
|
13
|
+
|
|
14
|
+
A plugin-based CLI wrapper ecosystem for common command-line tools. swisscli wraps native tools like `curl`, `wget`, `openssl`, `ssh-keygen`, `apt`, `dnf`, `yum`, `dpkg`, and `rpm`, providing consistent argument naming, subcommand interfaces, and automatic fallback between tools in the same category.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
git clone https://github.com/agonzalezrh/swisscli.git
|
|
20
|
+
cd swisscli
|
|
21
|
+
pip install .
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or install directly:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install swisscli
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
swisscli --help # list tools grouped by category
|
|
34
|
+
swisscli --version # show version
|
|
35
|
+
swisscli <tool> --help # tool-specific help
|
|
36
|
+
swisscli <tool> <subcommand> --help # subcommand details
|
|
37
|
+
swisscli <tool> [ARGS]... # run a tool
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Available tools
|
|
41
|
+
|
|
42
|
+
### web-cli
|
|
43
|
+
|
|
44
|
+
| Tool | Description |
|
|
45
|
+
|------|-------------|
|
|
46
|
+
| `curl` | HTTP requests with long-flag aliases (`--insecure` → `-k`, `--silent` → `-s`, `--location` → `-L`, etc.) |
|
|
47
|
+
| `wget` | File downloads with long-flag aliases (`--insecure` → `--no-check-certificate`, `--quiet` → `-q`, etc.) |
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
swisscli curl --insecure --location https://example.com
|
|
51
|
+
swisscli wget --output file.zip https://example.com/file.zip
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### ssl
|
|
55
|
+
|
|
56
|
+
| Tool | Subcommands | Description |
|
|
57
|
+
|------|-------------|-------------|
|
|
58
|
+
| `openssl` | `show`, `generate` | OpenSSL certificate toolkit |
|
|
59
|
+
| `ssh-keygen` | `show`, `generate` | SSH key management tool |
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
swisscli openssl show cert.pem
|
|
63
|
+
swisscli openssl generate --days 730 --out mycert.pem
|
|
64
|
+
swisscli ssh-keygen show ~/.ssh/id_rsa
|
|
65
|
+
swisscli ssh-keygen generate --type ed25519 --file mykey
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### package
|
|
69
|
+
|
|
70
|
+
| Tool | Subcommands | Description |
|
|
71
|
+
|------|-------------|-------------|
|
|
72
|
+
| `apt` | `install`, `remove`, `search`, `list` | Debian/Ubuntu package manager |
|
|
73
|
+
| `dnf` | `install`, `remove`, `search`, `list` | RPM package manager (modern) |
|
|
74
|
+
| `yum` | `install`, `remove`, `search`, `list` | RPM package manager (legacy) |
|
|
75
|
+
| `dpkg` | `list`, `info`, `search`, `contents` | Debian low-level package manager |
|
|
76
|
+
| `rpm` | `list`, `info`, `search`, `files` | RPM low-level package manager |
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
swisscli apt install curl
|
|
80
|
+
swisscli apt remove --purge firefox
|
|
81
|
+
swisscli apt search build-essential
|
|
82
|
+
swisscli dpkg list
|
|
83
|
+
swisscli dpkg info bash
|
|
84
|
+
swisscli rpm list
|
|
85
|
+
swisscli rpm files coreutils
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Fallback behavior
|
|
89
|
+
|
|
90
|
+
If a tool binary is not found on the system, swisscli automatically falls back to another tool in the same category. For example, running `swisscli dnf install curl` on a Debian system will fall back to `apt install curl`. The fallback notice is printed to stderr.
|
|
91
|
+
|
|
92
|
+
## Adding your own plugin
|
|
93
|
+
|
|
94
|
+
Create a file at `src/swisscli/plugins/<category>/<tool>.py`. Define a `Plugin` subclass with a `name`, `description`, and `category`. Export a module-level `plugin = MyPlugin()` instance — auto-discovery picks it up on next run.
|
|
95
|
+
|
|
96
|
+
Two patterns:
|
|
97
|
+
- **Simple flag mapping** — override `arg_mapper` for long-flag aliases
|
|
98
|
+
- **Subcommand-based** — override `subcommands` dict with `Subcommand` objects (prefix args, suffix args, optional arg mapper)
|
|
99
|
+
|
|
100
|
+
## Architecture
|
|
101
|
+
|
|
102
|
+
- **Plugin** — base class wrapping a CLI tool (argument mapping, execution, fallback)
|
|
103
|
+
- **ArgMapper** — translates user-friendly long flags to native tool flags
|
|
104
|
+
- **Subcommand** — defines prefix/suffix args injected between tool name and user args
|
|
105
|
+
- **PluginRegistry** — singleton registry discovered automatically via `pkgutil.walk_packages`
|
|
106
|
+
- **Categories** — plugins grouped by domain; fallback operates within the same category
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
swisscli/__init__.py,sha256=viHQ61Icyz3yboNr7qWbsqwFfye-lROlZjFLrr7KFCw,143
|
|
2
|
+
swisscli/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
|
|
3
|
+
swisscli/cli.py,sha256=R2o-33fiImCxyhnYzuLiTBDOsbFoTUVs1igEsOXMhGU,2024
|
|
4
|
+
swisscli/loader.py,sha256=Q9V60MBhhETTEeGnl0hAyyKhUXcPgH2CFw-JUgsG8AU,771
|
|
5
|
+
swisscli/plugin.py,sha256=DAaW5IZvV9ktxLN8WVpLJQ8FVx7EoPiH_ee1-jQW57E,4945
|
|
6
|
+
swisscli/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
swisscli/plugins/package/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
swisscli/plugins/package/apt.py,sha256=WfRM_t6oZC7nYPR02zbu5YWn-1g_i3B6F1cv2zMpZlk,1452
|
|
9
|
+
swisscli/plugins/package/dnf.py,sha256=0B7kQRvCpCYxLOON-qIM4bW-SM4irkdEpBq234_lfnc,1231
|
|
10
|
+
swisscli/plugins/package/dpkg.py,sha256=mgQQnPVzagQjVm3gbPgM0aabp5JwkLlzclmTzU5o2P4,959
|
|
11
|
+
swisscli/plugins/package/rpm.py,sha256=XsA5zoX6afphr9KOLsZv72Inc8lVyj0yjp7OpJOXO-k,933
|
|
12
|
+
swisscli/plugins/package/yum.py,sha256=Isp0rbRmxaXr3ERM4ZwNUDVqFGLiXmvfODJ2JljGCGs,1229
|
|
13
|
+
swisscli/plugins/ssl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
swisscli/plugins/ssl/openssl.py,sha256=EphBBnFQJsATrfFoChZ59RZHo102VEusiUq-kw2c_CM,1131
|
|
15
|
+
swisscli/plugins/ssl/ssh_keygen.py,sha256=jLhSn6YdfNXuc38-mvH9qLMow62HNwu4QY04mpQn1Mk,878
|
|
16
|
+
swisscli/plugins/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
swisscli/plugins/web/curl.py,sha256=Jo15xluFcCM_BkYXntPpiQVt2RvQV0UgTaofs6PXfRU,460
|
|
18
|
+
swisscli/plugins/web/wget.py,sha256=Zbt5nu42lkvcKip5xZUnA_LuFFSzDUrchkUcFS5dkdc,424
|
|
19
|
+
swisscli-0.1.0.dist-info/METADATA,sha256=jG-Wi3TaXjV8m9YyV5jdBgrxuAcnYjho9EaE9BAzT_k,3904
|
|
20
|
+
swisscli-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
21
|
+
swisscli-0.1.0.dist-info/entry_points.txt,sha256=UsdGA1PzWDfMp1TpiVgx-gsWhkU2GDGflwjMcyJP_0g,47
|
|
22
|
+
swisscli-0.1.0.dist-info/RECORD,,
|