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 ADDED
@@ -0,0 +1,8 @@
1
+ """remo - Remote development environment CLI."""
2
+
3
+ from importlib.metadata import version, PackageNotFoundError
4
+
5
+ try:
6
+ __version__ = version("remo-cli")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0-dev"
remo_cli/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Enable `python -m remo`."""
2
+
3
+ from remo_cli.cli.main import cli
4
+
5
+ cli()
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