remo-cli 0.8.0rc1__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.
- remo_cli/__init__.py +8 -0
- remo_cli/__main__.py +5 -0
- remo_cli/cli/__init__.py +0 -0
- remo_cli/cli/cp.py +161 -0
- remo_cli/cli/main.py +50 -0
- remo_cli/cli/providers/__init__.py +0 -0
- remo_cli/cli/providers/aws.py +153 -0
- remo_cli/cli/providers/hetzner.py +108 -0
- remo_cli/cli/providers/incus.py +132 -0
- remo_cli/cli/shell.py +32 -0
- remo_cli/core/__init__.py +0 -0
- remo_cli/core/ansible_runner.py +286 -0
- remo_cli/core/config.py +89 -0
- remo_cli/core/known_hosts.py +253 -0
- remo_cli/core/output.py +56 -0
- remo_cli/core/picker.py +57 -0
- remo_cli/core/rsync.py +104 -0
- remo_cli/core/ssh.py +344 -0
- remo_cli/core/validation.py +68 -0
- remo_cli/core/version.py +166 -0
- remo_cli/models/__init__.py +0 -0
- remo_cli/models/host.py +112 -0
- remo_cli/providers/__init__.py +0 -0
- remo_cli/providers/aws.py +955 -0
- remo_cli/providers/hetzner.py +302 -0
- remo_cli/providers/incus.py +389 -0
- remo_cli-0.8.0rc1.dist-info/METADATA +235 -0
- remo_cli-0.8.0rc1.dist-info/RECORD +31 -0
- remo_cli-0.8.0rc1.dist-info/WHEEL +4 -0
- remo_cli-0.8.0rc1.dist-info/entry_points.txt +3 -0
- remo_cli-0.8.0rc1.dist-info/licenses/LICENSE +21 -0
remo_cli/__init__.py
ADDED
remo_cli/__main__.py
ADDED
remo_cli/cli/__init__.py
ADDED
|
File without changes
|
remo_cli/cli/cp.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""remo cp command - Copy files to/from a remote environment."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from remo_cli.core.known_hosts import resolve_remo_host_by_name
|
|
12
|
+
from remo_cli.core.output import print_error, print_info
|
|
13
|
+
from remo_cli.core.rsync import transfer
|
|
14
|
+
from remo_cli.core.ssh import build_ssh_opts, resolve_remo_host
|
|
15
|
+
|
|
16
|
+
# Pattern for named remote specs: name must be 2+ chars starting with
|
|
17
|
+
# alphanumeric, followed by colon and a non-empty path. Single-letter
|
|
18
|
+
# prefixes are excluded to avoid matching Windows drive letters like C:.
|
|
19
|
+
_NAMED_REMOTE_RE = re.compile(r"^([a-zA-Z0-9][a-zA-Z0-9._-]+):(.+)$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_remote_spec(arg: str) -> tuple[str, str, str]:
|
|
23
|
+
"""Parse a single path argument for colon notation.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
tuple[str, str, str]
|
|
28
|
+
``(spec_type, env_name, path)`` where *spec_type* is ``"local"`` or
|
|
29
|
+
``"remote"``, *env_name* is the environment name (empty string for
|
|
30
|
+
bare-colon specs), and *path* is the file path component.
|
|
31
|
+
"""
|
|
32
|
+
# :path -> remote with bare colon (auto-select env)
|
|
33
|
+
if arg.startswith(":"):
|
|
34
|
+
return ("remote", "", arg[1:])
|
|
35
|
+
|
|
36
|
+
# name:path -> remote with named env
|
|
37
|
+
m = _NAMED_REMOTE_RE.match(arg)
|
|
38
|
+
if m:
|
|
39
|
+
return ("remote", m.group(1), m.group(2))
|
|
40
|
+
|
|
41
|
+
# Everything else -> local
|
|
42
|
+
return ("local", "", arg)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@click.command()
|
|
46
|
+
@click.option("-r", "--recursive", is_flag=True, default=False, help="Copy directories recursively")
|
|
47
|
+
@click.option("--progress", is_flag=True, default=False, help="Show transfer progress")
|
|
48
|
+
@click.argument("args", nargs=-1, required=True)
|
|
49
|
+
def cp(recursive: bool, progress: bool, args: tuple[str, ...]) -> None:
|
|
50
|
+
"""Copy files to/from a remo environment.
|
|
51
|
+
|
|
52
|
+
\b
|
|
53
|
+
Use colon notation for remote paths:
|
|
54
|
+
:path Remote path (auto-select environment)
|
|
55
|
+
name:path Remote path on named environment
|
|
56
|
+
|
|
57
|
+
\b
|
|
58
|
+
Upload: remo cp ./file.txt :/tmp/
|
|
59
|
+
Download: remo cp :/var/log/app.log ./
|
|
60
|
+
"""
|
|
61
|
+
# --- Phase 1: Validate positional args (need at least 2) ---
|
|
62
|
+
if len(args) < 2:
|
|
63
|
+
print_error("Expected at least 2 arguments: source(s) and destination.")
|
|
64
|
+
click.echo()
|
|
65
|
+
click.echo("Usage: remo cp [options] <source>... <destination>")
|
|
66
|
+
click.echo("Run 'remo cp --help' for examples.")
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
# --- Phase 2: Last arg = destination, rest = sources ---
|
|
70
|
+
dest_arg = args[-1]
|
|
71
|
+
source_args = args[:-1]
|
|
72
|
+
|
|
73
|
+
# --- Phase 3: Parse all specs ---
|
|
74
|
+
src_specs = [parse_remote_spec(s) for s in source_args]
|
|
75
|
+
dest_type, dest_env_name, dest_path = parse_remote_spec(dest_arg)
|
|
76
|
+
|
|
77
|
+
# --- Phase 4: Determine direction and validate consistency ---
|
|
78
|
+
has_remote_src = any(spec[0] == "remote" for spec in src_specs)
|
|
79
|
+
has_local_src = any(spec[0] == "local" for spec in src_specs)
|
|
80
|
+
|
|
81
|
+
if has_remote_src and has_local_src:
|
|
82
|
+
print_error("Cannot mix local and remote sources.")
|
|
83
|
+
click.echo("All sources must be either local or remote.")
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
if has_remote_src and dest_type == "remote":
|
|
87
|
+
print_error("Cannot copy from remote to remote.")
|
|
88
|
+
click.echo("One side must be local.")
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
|
|
91
|
+
if not has_remote_src and dest_type == "local":
|
|
92
|
+
print_error("No remote side specified.")
|
|
93
|
+
click.echo()
|
|
94
|
+
click.echo("Use colon notation for remote paths:")
|
|
95
|
+
click.echo(" remo cp ./file.txt :/tmp/ # upload")
|
|
96
|
+
click.echo(" remo cp :/var/log/app.log ./ # download")
|
|
97
|
+
click.echo()
|
|
98
|
+
click.echo("Run 'remo cp --help' for more examples.")
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
|
|
101
|
+
if has_remote_src:
|
|
102
|
+
direction = "download"
|
|
103
|
+
# Validate all remote sources reference the same environment.
|
|
104
|
+
remote_env_name = ""
|
|
105
|
+
for spec_type, env_name, _path in src_specs:
|
|
106
|
+
if spec_type == "remote":
|
|
107
|
+
if not remote_env_name:
|
|
108
|
+
remote_env_name = env_name
|
|
109
|
+
elif env_name != remote_env_name:
|
|
110
|
+
print_error("All remote sources must reference the same environment.")
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
src_paths = [spec[2] for spec in src_specs]
|
|
113
|
+
else:
|
|
114
|
+
direction = "upload"
|
|
115
|
+
remote_env_name = dest_env_name
|
|
116
|
+
src_paths = [spec[2] for spec in src_specs]
|
|
117
|
+
|
|
118
|
+
# --- Phase 5: Resolve environment ---
|
|
119
|
+
if remote_env_name:
|
|
120
|
+
host = resolve_remo_host_by_name(remote_env_name)
|
|
121
|
+
else:
|
|
122
|
+
host = resolve_remo_host()
|
|
123
|
+
|
|
124
|
+
# --- Phase 6: Build SSH opts ---
|
|
125
|
+
ssh_opts, ssh_target = build_ssh_opts(host)
|
|
126
|
+
|
|
127
|
+
# --- Phase 7: Validate local sources (upload only) ---
|
|
128
|
+
if direction == "upload":
|
|
129
|
+
for src in src_paths:
|
|
130
|
+
if not os.path.exists(src):
|
|
131
|
+
print_error(f"Source not found: {src}")
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
if os.path.isdir(src) and not recursive:
|
|
134
|
+
print_error(f"'{src}' is a directory. Use -r to copy directories.")
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
|
|
137
|
+
# --- Phase 8: Build and execute rsync ---
|
|
138
|
+
if direction == "upload":
|
|
139
|
+
print_info(f"Uploading to {host.type}: {host.name}...")
|
|
140
|
+
rc = transfer(
|
|
141
|
+
ssh_opts=ssh_opts,
|
|
142
|
+
ssh_target=ssh_target,
|
|
143
|
+
sources=src_paths,
|
|
144
|
+
dest=f"{ssh_target}:{dest_path}",
|
|
145
|
+
recursive=recursive,
|
|
146
|
+
progress=progress,
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
print_info(f"Downloading from {host.type}: {host.name}...")
|
|
150
|
+
remote_sources = [f"{ssh_target}:{p}" for p in src_paths]
|
|
151
|
+
rc = transfer(
|
|
152
|
+
ssh_opts=ssh_opts,
|
|
153
|
+
ssh_target=ssh_target,
|
|
154
|
+
sources=remote_sources,
|
|
155
|
+
dest=dest_path,
|
|
156
|
+
recursive=recursive,
|
|
157
|
+
progress=progress,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if rc != 0:
|
|
161
|
+
sys.exit(rc)
|
remo_cli/cli/main.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Root CLI group for remo."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
import remo_cli
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
11
|
+
@click.version_option(
|
|
12
|
+
version=remo_cli.__version__, prog_name="remo", message="%(prog)s %(version)s"
|
|
13
|
+
)
|
|
14
|
+
def cli() -> None:
|
|
15
|
+
"""Remote development environment CLI."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@cli.result_callback()
|
|
19
|
+
def _post_command_hook(result: object, **kwargs: object) -> None:
|
|
20
|
+
"""Run passive update check after every command."""
|
|
21
|
+
try:
|
|
22
|
+
from remo_cli.core.version import check_for_updates_passive
|
|
23
|
+
from remo_cli.core.output import print_info
|
|
24
|
+
|
|
25
|
+
hint = check_for_updates_passive()
|
|
26
|
+
if hint:
|
|
27
|
+
print()
|
|
28
|
+
print_info(hint)
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _register_commands() -> None:
|
|
34
|
+
"""Register all subcommands and groups. Called at import time."""
|
|
35
|
+
# Import lazily to avoid circular imports and to keep startup fast
|
|
36
|
+
# when only --version or --help is requested.
|
|
37
|
+
from remo_cli.cli.shell import shell # noqa: F811
|
|
38
|
+
from remo_cli.cli.cp import cp # noqa: F811
|
|
39
|
+
from remo_cli.cli.providers.incus import incus # noqa: F811
|
|
40
|
+
from remo_cli.cli.providers.hetzner import hetzner # noqa: F811
|
|
41
|
+
from remo_cli.cli.providers.aws import aws # noqa: F811
|
|
42
|
+
|
|
43
|
+
cli.add_command(shell)
|
|
44
|
+
cli.add_command(cp)
|
|
45
|
+
cli.add_command(incus)
|
|
46
|
+
cli.add_command(hetzner)
|
|
47
|
+
cli.add_command(aws)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_register_commands()
|
|
File without changes
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""remo aws commands - Manage AWS EC2 instances."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group()
|
|
11
|
+
def aws() -> None:
|
|
12
|
+
"""Manage AWS EC2 instances with EBS storage."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@aws.command()
|
|
16
|
+
@click.option("--name", default="", help="Instance name (defaults to $USER).")
|
|
17
|
+
@click.option("--type", "instance_type", default="", help="EC2 instance type.")
|
|
18
|
+
@click.option("--region", default="", help="AWS region.")
|
|
19
|
+
@click.option("--volume-size", default="", help="EBS volume size in GB.")
|
|
20
|
+
@click.option("--spot", is_flag=True, default=False, help="Use spot instance.")
|
|
21
|
+
@click.option("--iam-profile", default="", help="IAM instance profile name.")
|
|
22
|
+
@click.option("--only", multiple=True, help="Only configure these tools.")
|
|
23
|
+
@click.option("--skip", multiple=True, help="Skip configuring these tools.")
|
|
24
|
+
@click.option("--yes", "-y", "auto_confirm", is_flag=True, default=False, help="Skip confirmation prompts.")
|
|
25
|
+
@click.option("-v", "--verbose", is_flag=True, default=False, help="Verbose output.")
|
|
26
|
+
def create(
|
|
27
|
+
name: str,
|
|
28
|
+
instance_type: str,
|
|
29
|
+
region: str,
|
|
30
|
+
volume_size: str,
|
|
31
|
+
spot: bool,
|
|
32
|
+
iam_profile: str,
|
|
33
|
+
only: tuple[str, ...],
|
|
34
|
+
skip: tuple[str, ...],
|
|
35
|
+
auto_confirm: bool,
|
|
36
|
+
verbose: bool,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Create a new AWS EC2 instance."""
|
|
39
|
+
from remo_cli.providers.aws import create as aws_create
|
|
40
|
+
|
|
41
|
+
rc = aws_create(
|
|
42
|
+
name=name,
|
|
43
|
+
instance_type=instance_type,
|
|
44
|
+
region=region,
|
|
45
|
+
volume_size=volume_size,
|
|
46
|
+
use_spot=spot,
|
|
47
|
+
iam_profile=iam_profile,
|
|
48
|
+
tools_only=only,
|
|
49
|
+
tools_skip=skip,
|
|
50
|
+
verbose=verbose,
|
|
51
|
+
)
|
|
52
|
+
sys.exit(rc)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@aws.command()
|
|
56
|
+
@click.option("--name", default="", help="Instance name (defaults to $USER).")
|
|
57
|
+
@click.option("--remove-storage", is_flag=True, default=False, help="Also remove EBS storage volume.")
|
|
58
|
+
@click.option("--yes", "-y", "auto_confirm", is_flag=True, default=False, help="Skip confirmation prompts.")
|
|
59
|
+
@click.option("-v", "--verbose", is_flag=True, default=False, help="Verbose output.")
|
|
60
|
+
def destroy(
|
|
61
|
+
name: str,
|
|
62
|
+
remove_storage: bool,
|
|
63
|
+
auto_confirm: bool,
|
|
64
|
+
verbose: bool,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Destroy an AWS EC2 instance."""
|
|
67
|
+
from remo_cli.providers.aws import destroy as aws_destroy
|
|
68
|
+
|
|
69
|
+
rc = aws_destroy(
|
|
70
|
+
name=name,
|
|
71
|
+
auto_confirm=auto_confirm,
|
|
72
|
+
remove_storage=remove_storage,
|
|
73
|
+
verbose=verbose,
|
|
74
|
+
)
|
|
75
|
+
sys.exit(rc)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@aws.command()
|
|
79
|
+
@click.option("--name", default="", help="Instance name (defaults to $USER).")
|
|
80
|
+
@click.option("--only", multiple=True, help="Only configure these tools.")
|
|
81
|
+
@click.option("--skip", multiple=True, help="Skip configuring these tools.")
|
|
82
|
+
@click.option("-v", "--verbose", is_flag=True, default=False, help="Verbose output.")
|
|
83
|
+
def update(
|
|
84
|
+
name: str,
|
|
85
|
+
only: tuple[str, ...],
|
|
86
|
+
skip: tuple[str, ...],
|
|
87
|
+
verbose: bool,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Re-configure dev tools on an existing AWS instance."""
|
|
90
|
+
from remo_cli.providers.aws import update as aws_update
|
|
91
|
+
|
|
92
|
+
rc = aws_update(
|
|
93
|
+
name=name,
|
|
94
|
+
tools_only=only,
|
|
95
|
+
tools_skip=skip,
|
|
96
|
+
verbose=verbose,
|
|
97
|
+
)
|
|
98
|
+
sys.exit(rc)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@aws.command("list")
|
|
102
|
+
def list_cmd() -> None:
|
|
103
|
+
"""List registered AWS instances."""
|
|
104
|
+
from remo_cli.providers.aws import list_hosts
|
|
105
|
+
|
|
106
|
+
list_hosts()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@aws.command()
|
|
110
|
+
@click.option("--region", default="", help="AWS region to sync.")
|
|
111
|
+
def sync(region: str) -> None:
|
|
112
|
+
"""Sync local registry with running AWS instances."""
|
|
113
|
+
from remo_cli.providers.aws import sync as aws_sync
|
|
114
|
+
|
|
115
|
+
aws_sync(region=region)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@aws.command()
|
|
119
|
+
@click.option("--name", default="", help="Instance name (defaults to $USER).")
|
|
120
|
+
@click.option("--yes", "-y", "auto_confirm", is_flag=True, default=False, help="Skip confirmation prompts.")
|
|
121
|
+
def stop(name: str, auto_confirm: bool) -> None:
|
|
122
|
+
"""Stop an AWS EC2 instance."""
|
|
123
|
+
from remo_cli.providers.aws import stop as aws_stop
|
|
124
|
+
|
|
125
|
+
aws_stop(name=name, auto_confirm=auto_confirm)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@aws.command()
|
|
129
|
+
@click.option("--name", default="", help="Instance name (defaults to $USER).")
|
|
130
|
+
def start(name: str) -> None:
|
|
131
|
+
"""Start a stopped AWS EC2 instance."""
|
|
132
|
+
from remo_cli.providers.aws import start as aws_start
|
|
133
|
+
|
|
134
|
+
aws_start(name=name)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@aws.command()
|
|
138
|
+
@click.option("--name", default="", help="Instance name (defaults to $USER).")
|
|
139
|
+
@click.option("--yes", "-y", "auto_confirm", is_flag=True, default=False, help="Skip confirmation prompts.")
|
|
140
|
+
def reboot(name: str, auto_confirm: bool) -> None:
|
|
141
|
+
"""Reboot an AWS EC2 instance."""
|
|
142
|
+
from remo_cli.providers.aws import reboot as aws_reboot
|
|
143
|
+
|
|
144
|
+
aws_reboot(name=name, auto_confirm=auto_confirm)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@aws.command()
|
|
148
|
+
@click.option("--name", default="", help="Instance name (defaults to $USER).")
|
|
149
|
+
def info(name: str) -> None:
|
|
150
|
+
"""Show detailed info about an AWS EC2 instance."""
|
|
151
|
+
from remo_cli.providers.aws import info as aws_info
|
|
152
|
+
|
|
153
|
+
aws_info(name=name)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""remo hetzner commands - Manage Hetzner Cloud VMs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group()
|
|
11
|
+
def hetzner() -> None:
|
|
12
|
+
"""Manage Hetzner Cloud VMs."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@hetzner.command()
|
|
16
|
+
@click.option("--name", default="", help="Server name (default: remote-coding-server).")
|
|
17
|
+
@click.option("--type", "server_type", default="", help="Server type (default: cx22).")
|
|
18
|
+
@click.option("--location", default="", help="Location (default: hel1).")
|
|
19
|
+
@click.option("--volume-size", default="", help="Volume size in GB (default: 10).")
|
|
20
|
+
@click.option("--only", multiple=True, help="Only install these tools.")
|
|
21
|
+
@click.option("--skip", multiple=True, help="Skip these tools.")
|
|
22
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompts.")
|
|
23
|
+
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose output.")
|
|
24
|
+
def create(
|
|
25
|
+
name: str,
|
|
26
|
+
server_type: str,
|
|
27
|
+
location: str,
|
|
28
|
+
volume_size: str,
|
|
29
|
+
only: tuple[str, ...],
|
|
30
|
+
skip: tuple[str, ...],
|
|
31
|
+
yes: bool,
|
|
32
|
+
verbose: bool,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Provision a new Hetzner Cloud VM."""
|
|
35
|
+
from remo_cli.providers.hetzner import create as do_create
|
|
36
|
+
|
|
37
|
+
rc = do_create(
|
|
38
|
+
name=name,
|
|
39
|
+
server_type=server_type,
|
|
40
|
+
location=location,
|
|
41
|
+
volume_size=volume_size,
|
|
42
|
+
tools_only=only,
|
|
43
|
+
tools_skip=skip,
|
|
44
|
+
verbose=verbose,
|
|
45
|
+
)
|
|
46
|
+
sys.exit(rc)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@hetzner.command()
|
|
50
|
+
@click.option("--name", default="", help="Server name (default: remote-coding-server).")
|
|
51
|
+
@click.option("--remove-volume", is_flag=True, help="Also remove persistent volume.")
|
|
52
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompts.")
|
|
53
|
+
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose output.")
|
|
54
|
+
def destroy(
|
|
55
|
+
name: str,
|
|
56
|
+
remove_volume: bool,
|
|
57
|
+
yes: bool,
|
|
58
|
+
verbose: bool,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Tear down a Hetzner Cloud VM."""
|
|
61
|
+
from remo_cli.providers.hetzner import destroy as do_destroy
|
|
62
|
+
|
|
63
|
+
rc = do_destroy(
|
|
64
|
+
name=name,
|
|
65
|
+
auto_confirm=yes,
|
|
66
|
+
remove_volume=remove_volume,
|
|
67
|
+
verbose=verbose,
|
|
68
|
+
)
|
|
69
|
+
sys.exit(rc)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@hetzner.command()
|
|
73
|
+
@click.option("--name", default="", help="Server name (default: remote-coding-server).")
|
|
74
|
+
@click.option("--only", multiple=True, help="Only install these tools.")
|
|
75
|
+
@click.option("--skip", multiple=True, help="Skip these tools.")
|
|
76
|
+
@click.option("-v", "--verbose", is_flag=True, help="Enable verbose output.")
|
|
77
|
+
def update(
|
|
78
|
+
name: str,
|
|
79
|
+
only: tuple[str, ...],
|
|
80
|
+
skip: tuple[str, ...],
|
|
81
|
+
verbose: bool,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Update dev tools on an existing VM."""
|
|
84
|
+
from remo_cli.providers.hetzner import update as do_update
|
|
85
|
+
|
|
86
|
+
rc = do_update(
|
|
87
|
+
name=name,
|
|
88
|
+
tools_only=only,
|
|
89
|
+
tools_skip=skip,
|
|
90
|
+
verbose=verbose,
|
|
91
|
+
)
|
|
92
|
+
sys.exit(rc)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@hetzner.command("list")
|
|
96
|
+
def list_cmd() -> None:
|
|
97
|
+
"""List registered Hetzner VMs."""
|
|
98
|
+
from remo_cli.providers.hetzner import list_hosts
|
|
99
|
+
|
|
100
|
+
list_hosts()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@hetzner.command()
|
|
104
|
+
def sync() -> None:
|
|
105
|
+
"""Discover VMs and update registry."""
|
|
106
|
+
from remo_cli.providers.hetzner import sync as do_sync
|
|
107
|
+
|
|
108
|
+
do_sync()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""remo incus commands - Manage Incus containers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from remo_cli.providers import incus as providers_incus
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group()
|
|
13
|
+
def incus() -> None:
|
|
14
|
+
"""Manage Incus containers (local or remote host)."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@incus.command()
|
|
18
|
+
@click.option("--name", default="dev1", help="Container name (default: dev1).")
|
|
19
|
+
@click.option("--host", default="localhost", help="Incus host (default: localhost).")
|
|
20
|
+
@click.option("--user", default="", help="SSH user for remote Incus host.")
|
|
21
|
+
@click.option("--domain", default="", help="Domain name for the container.")
|
|
22
|
+
@click.option("--image", default="", help="Container image to use.")
|
|
23
|
+
@click.option("--only", multiple=True, help="Only install these tools.")
|
|
24
|
+
@click.option("--skip", multiple=True, help="Skip these tools.")
|
|
25
|
+
@click.option("--yes", "-y", is_flag=True, help="Auto-confirm prompts.")
|
|
26
|
+
@click.option("-v", "--verbose", is_flag=True, help="Verbose output.")
|
|
27
|
+
def create(
|
|
28
|
+
name: str,
|
|
29
|
+
host: str,
|
|
30
|
+
user: str,
|
|
31
|
+
domain: str,
|
|
32
|
+
image: str,
|
|
33
|
+
only: tuple[str, ...],
|
|
34
|
+
skip: tuple[str, ...],
|
|
35
|
+
yes: bool,
|
|
36
|
+
verbose: bool,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Create an Incus container."""
|
|
39
|
+
rc = providers_incus.create(
|
|
40
|
+
name=name,
|
|
41
|
+
host=host,
|
|
42
|
+
user=user,
|
|
43
|
+
domain=domain,
|
|
44
|
+
image=image,
|
|
45
|
+
tools_only=only,
|
|
46
|
+
tools_skip=skip,
|
|
47
|
+
verbose=verbose,
|
|
48
|
+
)
|
|
49
|
+
sys.exit(rc)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@incus.command()
|
|
53
|
+
@click.option("--name", default="dev1", help="Container name (default: dev1).")
|
|
54
|
+
@click.option("--host", default="", help="Incus host (default: auto-detect).")
|
|
55
|
+
@click.option("--user", default="", help="SSH user for remote Incus host.")
|
|
56
|
+
@click.option("--remove-storage", is_flag=True, help="Also remove storage volume.")
|
|
57
|
+
@click.option("--yes", "-y", is_flag=True, help="Auto-confirm prompts.")
|
|
58
|
+
@click.option("-v", "--verbose", is_flag=True, help="Verbose output.")
|
|
59
|
+
def destroy(
|
|
60
|
+
name: str,
|
|
61
|
+
host: str,
|
|
62
|
+
user: str,
|
|
63
|
+
remove_storage: bool,
|
|
64
|
+
yes: bool,
|
|
65
|
+
verbose: bool,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Destroy an Incus container."""
|
|
68
|
+
rc = providers_incus.destroy(
|
|
69
|
+
name=name,
|
|
70
|
+
host=host,
|
|
71
|
+
user=user,
|
|
72
|
+
auto_confirm=yes,
|
|
73
|
+
verbose=verbose,
|
|
74
|
+
)
|
|
75
|
+
sys.exit(rc)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@incus.command()
|
|
79
|
+
@click.option("--name", default="dev1", help="Container name (default: dev1).")
|
|
80
|
+
@click.option("--host", default="", help="Incus host (default: auto-detect).")
|
|
81
|
+
@click.option("--user", default="", help="SSH user for remote Incus host.")
|
|
82
|
+
@click.option("--only", multiple=True, help="Only install these tools.")
|
|
83
|
+
@click.option("--skip", multiple=True, help="Skip these tools.")
|
|
84
|
+
@click.option("-v", "--verbose", is_flag=True, help="Verbose output.")
|
|
85
|
+
def update(
|
|
86
|
+
name: str,
|
|
87
|
+
host: str,
|
|
88
|
+
user: str,
|
|
89
|
+
only: tuple[str, ...],
|
|
90
|
+
skip: tuple[str, ...],
|
|
91
|
+
verbose: bool,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Update tools on an Incus container."""
|
|
94
|
+
rc = providers_incus.update(
|
|
95
|
+
name=name,
|
|
96
|
+
host=host,
|
|
97
|
+
user=user,
|
|
98
|
+
tools_only=only,
|
|
99
|
+
tools_skip=skip,
|
|
100
|
+
verbose=verbose,
|
|
101
|
+
)
|
|
102
|
+
sys.exit(rc)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@incus.command("list")
|
|
106
|
+
def list_cmd() -> None:
|
|
107
|
+
"""List registered Incus containers."""
|
|
108
|
+
providers_incus.list_hosts()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@incus.command()
|
|
112
|
+
@click.option("--host", default="localhost", help="Incus host (default: localhost).")
|
|
113
|
+
@click.option("--user", default="", help="SSH user for remote Incus host.")
|
|
114
|
+
def sync(host: str, user: str) -> None:
|
|
115
|
+
"""Discover containers from an Incus host."""
|
|
116
|
+
providers_incus.sync(host=host, user=user)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@incus.command()
|
|
120
|
+
@click.option("--host", default="localhost", help="Incus host (default: localhost).")
|
|
121
|
+
@click.option("--user", default="", help="SSH user for remote Incus host.")
|
|
122
|
+
@click.option("--network-type", default="", help="Network type for Incus host.")
|
|
123
|
+
@click.option("-v", "--verbose", is_flag=True, help="Verbose output.")
|
|
124
|
+
def bootstrap(host: str, user: str, network_type: str, verbose: bool) -> None:
|
|
125
|
+
"""Initialize an Incus host."""
|
|
126
|
+
rc = providers_incus.bootstrap(
|
|
127
|
+
host=host,
|
|
128
|
+
user=user,
|
|
129
|
+
network_type=network_type,
|
|
130
|
+
verbose=verbose,
|
|
131
|
+
)
|
|
132
|
+
sys.exit(rc)
|
remo_cli/cli/shell.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""remo shell command - Connect to a remote environment."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.command()
|
|
9
|
+
@click.argument("name", required=False, default=None)
|
|
10
|
+
@click.option(
|
|
11
|
+
"-L",
|
|
12
|
+
"tunnels",
|
|
13
|
+
multiple=True,
|
|
14
|
+
help="Forward port: PORT or LOCAL:REMOTE",
|
|
15
|
+
)
|
|
16
|
+
@click.option(
|
|
17
|
+
"--no-open",
|
|
18
|
+
is_flag=True,
|
|
19
|
+
default=False,
|
|
20
|
+
help="Skip auto-opening browser for tunneled ports",
|
|
21
|
+
)
|
|
22
|
+
def shell(name: str | None, tunnels: tuple[str, ...], no_open: bool) -> None:
|
|
23
|
+
"""Connect to a remo environment (auto-detects or picker)."""
|
|
24
|
+
from remo_cli.core.ssh import resolve_remo_host, shell_connect # noqa: PLC0415
|
|
25
|
+
from remo_cli.providers.aws import auto_start_aws_if_stopped # noqa: PLC0415
|
|
26
|
+
|
|
27
|
+
host = resolve_remo_host(name)
|
|
28
|
+
|
|
29
|
+
# Auto-start stopped AWS instances before connecting
|
|
30
|
+
host = auto_start_aws_if_stopped(host)
|
|
31
|
+
|
|
32
|
+
shell_connect(host, list(tunnels), no_open)
|
|
File without changes
|