evo-cli 0.1.11__tar.gz → 0.3.0__tar.gz

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.
Files changed (31) hide show
  1. {evo_cli-0.1.11 → evo_cli-0.3.0}/PKG-INFO +1 -1
  2. evo_cli-0.3.0/evo_cli/VERSION +1 -0
  3. {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/cli.py +4 -0
  4. {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/commands/cloudflare.py +238 -52
  5. evo_cli-0.3.0/evo_cli/commands/gdrive.py +528 -0
  6. evo_cli-0.3.0/evo_cli/commands/site2s.py +289 -0
  7. {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli.egg-info/PKG-INFO +1 -1
  8. {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli.egg-info/SOURCES.txt +2 -0
  9. evo_cli-0.1.11/evo_cli/VERSION +0 -1
  10. {evo_cli-0.1.11 → evo_cli-0.3.0}/Containerfile +0 -0
  11. {evo_cli-0.1.11 → evo_cli-0.3.0}/HISTORY.md +0 -0
  12. {evo_cli-0.1.11 → evo_cli-0.3.0}/LICENSE +0 -0
  13. {evo_cli-0.1.11 → evo_cli-0.3.0}/MANIFEST.in +0 -0
  14. {evo_cli-0.1.11 → evo_cli-0.3.0}/README.md +0 -0
  15. {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/__init__.py +0 -0
  16. {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/__main__.py +0 -0
  17. {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/base.py +0 -0
  18. {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/commands/__init__.py +0 -0
  19. {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/commands/fix_claude.py +0 -0
  20. {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/commands/miniconda.py +0 -0
  21. {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/commands/ssh.py +0 -0
  22. {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli/console.py +0 -0
  23. {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli.egg-info/dependency_links.txt +0 -0
  24. {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli.egg-info/entry_points.txt +0 -0
  25. {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli.egg-info/requires.txt +0 -0
  26. {evo_cli-0.1.11 → evo_cli-0.3.0}/evo_cli.egg-info/top_level.txt +0 -0
  27. {evo_cli-0.1.11 → evo_cli-0.3.0}/pyproject.toml +0 -0
  28. {evo_cli-0.1.11 → evo_cli-0.3.0}/setup.cfg +0 -0
  29. {evo_cli-0.1.11 → evo_cli-0.3.0}/tests/__init__.py +0 -0
  30. {evo_cli-0.1.11 → evo_cli-0.3.0}/tests/test_cli.py +0 -0
  31. {evo_cli-0.1.11 → evo_cli-0.3.0}/tests/test_fix_claude.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo_cli
3
- Version: 0.1.11
3
+ Version: 0.3.0
4
4
  Summary: Evolution CLI - a developer toolbox for setting up dev machines
5
5
  Author: maycuatroi
6
6
  Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
@@ -0,0 +1 @@
1
+ 0.3.0
@@ -3,7 +3,9 @@ import rich_click as click
3
3
  from evo_cli import __version__
4
4
  from evo_cli.commands.cloudflare import cfssh
5
5
  from evo_cli.commands.fix_claude import f_claude
6
+ from evo_cli.commands.gdrive import gdrive
6
7
  from evo_cli.commands.miniconda import miniconda
8
+ from evo_cli.commands.site2s import site2s
7
9
  from evo_cli.commands.ssh import setupssh
8
10
 
9
11
  click.rich_click.USE_MARKDOWN = True
@@ -31,6 +33,8 @@ cli.add_command(setupssh)
31
33
  cli.add_command(miniconda)
32
34
  cli.add_command(cfssh)
33
35
  cli.add_command(f_claude)
36
+ cli.add_command(gdrive)
37
+ cli.add_command(site2s)
34
38
 
35
39
 
36
40
  def main():
@@ -3,6 +3,7 @@ import json
3
3
  import os
4
4
  import platform
5
5
  import shutil
6
+ import socket
6
7
  import subprocess
7
8
  import tempfile
8
9
  from pathlib import Path
@@ -27,15 +28,26 @@ from evo_cli.console import (
27
28
  )
28
29
 
29
30
  CLOUDFLARED_RELEASE = "https://github.com/cloudflare/cloudflared/releases/latest/download"
31
+
32
+ # Linux (systemd) layout - config and credentials live under /etc, owned by root.
30
33
  ETC_DIR = Path("/etc/cloudflared")
31
34
  SERVICE_UNIT = Path("/etc/systemd/system/cloudflared.service")
32
35
  ROOT_PATH_DIRS = ("/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin")
33
36
 
37
+ # macOS (launchd) layout - cloudflared installs a per-user LaunchAgent that reads
38
+ # the config from the user's ~/.cloudflared. No root/sudo involved.
39
+ MAC_LABEL = "com.cloudflare.cloudflared"
40
+ MAC_PLIST = Path.home() / "Library" / "LaunchAgents" / f"{MAC_LABEL}.plist"
41
+
42
+ IS_MACOS = platform.system() == "Darwin"
43
+ IS_LINUX = platform.system() == "Linux"
44
+
34
45
  EPILOG = Text.from_markup(
35
46
  "[bold]Examples[/bold]\n\n"
36
- " [cyan]evo cfssh -H dev.example.com[/cyan]\n"
47
+ " [cyan]evo cfssh -H dev.example.com[/cyan] # SSH into this machine\n"
37
48
  " [cyan]evo cfssh -H box.example.com -n my-box -P 2222[/cyan]\n"
38
- " [cyan]evo cfssh -H dev.example.com --no-service[/cyan]"
49
+ " [cyan]evo cfssh -H app.example.com --http 3000[/cyan] # expose a local web app\n"
50
+ " [cyan]evo cfssh -H dev.example.com --no-service[/cyan] # configure only, run manually"
39
51
  )
40
52
 
41
53
 
@@ -64,14 +76,30 @@ def cloudflared_dir():
64
76
  return Path.home() / ".cloudflared"
65
77
 
66
78
 
67
- def build_config_yaml(tunnel_id, hostname, ssh_port, credentials_path):
79
+ def server_config_dir():
80
+ """Where the running service reads config.yml from on this OS."""
81
+ return cloudflared_dir() if IS_MACOS else ETC_DIR
82
+
83
+
84
+ def server_config_file():
85
+ return server_config_dir() / "config.yml"
86
+
87
+
88
+ def _port_listening(port):
89
+ """True if something is accepting TCP connections on localhost:port."""
90
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
91
+ sock.settimeout(0.5)
92
+ return sock.connect_ex(("127.0.0.1", int(port))) == 0
93
+
94
+
95
+ def build_config_yaml(tunnel_id, hostname, service, credentials_path):
68
96
  return (
69
97
  f"tunnel: {tunnel_id}\n"
70
98
  f"credentials-file: {credentials_path}\n"
71
99
  f"\n"
72
100
  f"ingress:\n"
73
101
  f" - hostname: {hostname}\n"
74
- f" service: ssh://localhost:{ssh_port}\n"
102
+ f" service: {service}\n"
75
103
  f" - service: http_status:404\n"
76
104
  )
77
105
 
@@ -82,6 +110,20 @@ def install_cloudflared():
82
110
  info(f"cloudflared already installed: [accent]{version}[/accent]")
83
111
  return True
84
112
 
113
+ if IS_MACOS:
114
+ if not _has("brew"):
115
+ error("cloudflared not found and Homebrew is unavailable.")
116
+ info(
117
+ "Install it manually: https://github.com/cloudflare/cloudflared/releases (or `brew install cloudflared`)."
118
+ )
119
+ return False
120
+ run_command(["brew", "install", "cloudflared"], check=False, status="Installing cloudflared (brew)")
121
+ if not _has("cloudflared"):
122
+ error("cloudflared installation failed.")
123
+ return False
124
+ success("cloudflared installed.")
125
+ return True
126
+
85
127
  arch = _deb_arch()
86
128
  url = f"{CLOUDFLARED_RELEASE}/cloudflared-linux-{arch}.deb"
87
129
  with tempfile.TemporaryDirectory() as tmp:
@@ -142,7 +184,7 @@ def find_tunnel(name):
142
184
 
143
185
 
144
186
  def read_local_config():
145
- config_file = ETC_DIR / "config.yml"
187
+ config_file = server_config_file()
146
188
  if not config_file.exists():
147
189
  return None
148
190
  try:
@@ -150,7 +192,7 @@ def read_local_config():
150
192
  except OSError:
151
193
  return None
152
194
  tunnel_id = None
153
- ssh_hostnames = []
195
+ routes = [] # list of (hostname, service)
154
196
  current_host = None
155
197
  for raw in text.splitlines():
156
198
  line = raw.strip()
@@ -159,15 +201,17 @@ def read_local_config():
159
201
  elif line.startswith("- hostname:") or line.startswith("hostname:"):
160
202
  current_host = line.split(":", 1)[1].strip()
161
203
  elif line.startswith("service:"):
162
- if current_host and line.split(":", 1)[1].strip().startswith("ssh://"):
163
- ssh_hostnames.append(current_host)
204
+ if current_host:
205
+ routes.append((current_host, line.split(":", 1)[1].strip()))
164
206
  current_host = None
165
207
  if not tunnel_id:
166
208
  return None
167
- return {"config_file": config_file, "tunnel_id": tunnel_id, "ssh_hostnames": ssh_hostnames}
209
+ return {"config_file": config_file, "tunnel_id": tunnel_id, "routes": routes}
168
210
 
169
211
 
170
212
  def cloudflared_service_state():
213
+ if IS_MACOS:
214
+ return mac_service_state() if MAC_PLIST.exists() else None
171
215
  if not SERVICE_UNIT.exists():
172
216
  return None
173
217
  try:
@@ -177,6 +221,19 @@ def cloudflared_service_state():
177
221
  return result.stdout.strip() or "unknown"
178
222
 
179
223
 
224
+ def mac_service_state():
225
+ """Read the launchd state of the cloudflared user agent."""
226
+ if not MAC_PLIST.exists():
227
+ return None
228
+ try:
229
+ result = subprocess.run(["launchctl", "list", MAC_LABEL], capture_output=True, text=True)
230
+ except OSError:
231
+ return "unknown"
232
+ if result.returncode != 0:
233
+ return "stopped"
234
+ return "running" if '"PID"' in result.stdout else "loaded"
235
+
236
+
180
237
  def detect_local_tunnel():
181
238
  config = read_local_config()
182
239
  if not config:
@@ -197,11 +254,10 @@ def show_local_tunnel(existing):
197
254
  table.add_row("config", str(existing["config_file"]))
198
255
  name = existing["name"] or "[dim]not found in account[/dim]"
199
256
  table.add_row("tunnel", f"[accent]{name}[/accent] ({existing['tunnel_id']})")
200
- table.add_row("ssh hostname", ", ".join(existing["ssh_hostnames"]) or "[dim]none[/dim]")
257
+ routes = ", ".join(f"{host} -> {svc}" for host, svc in existing["routes"]) or "[dim]none[/dim]"
258
+ table.add_row("routes", routes)
201
259
  table.add_row("service", existing["service"] or "not installed")
202
- console.print(
203
- Panel(table, title="SSH tunnel already configured on this machine", border_style="warning", expand=False)
204
- )
260
+ console.print(Panel(table, title="Tunnel already configured on this machine", border_style="warning", expand=False))
205
261
 
206
262
 
207
263
  def ensure_tunnel(name):
@@ -218,7 +274,19 @@ def ensure_tunnel(name):
218
274
  return tunnel_id
219
275
 
220
276
 
221
- def write_server_config(tunnel_id, hostname, ssh_port):
277
+ def _print_config_panel(config_file, config_text):
278
+ success(f"Wrote {config_file}")
279
+ console.print(
280
+ Panel(
281
+ Syntax(config_text.rstrip(), "yaml", theme="ansi_dark", background_color="default"),
282
+ title=str(config_file),
283
+ border_style="step",
284
+ expand=False,
285
+ )
286
+ )
287
+
288
+
289
+ def write_server_config(tunnel_id, hostname, service):
222
290
  local_cred = cloudflared_dir() / f"{tunnel_id}.json"
223
291
  if not local_cred.exists():
224
292
  error(f"Credentials file {local_cred} not found.")
@@ -227,9 +295,28 @@ def write_server_config(tunnel_id, hostname, ssh_port):
227
295
  info("or run this command on the machine that created the tunnel.")
228
296
  return None
229
297
 
298
+ if IS_MACOS:
299
+ return _write_server_config_macos(tunnel_id, hostname, service, local_cred)
300
+ return _write_server_config_linux(tunnel_id, hostname, service, local_cred)
301
+
302
+
303
+ def _write_server_config_macos(tunnel_id, hostname, service, local_cred):
304
+ # The LaunchAgent runs as this user, so it reads ~/.cloudflared directly -
305
+ # no copy into /etc and no sudo needed.
306
+ config_file = cloudflared_dir() / "config.yml"
307
+ config_text = build_config_yaml(tunnel_id, hostname, service, str(local_cred))
308
+ if config_file.exists():
309
+ shutil.copy2(config_file, str(config_file) + ".bak")
310
+ info(f"Backed up existing config to {config_file}.bak")
311
+ config_file.write_text(config_text)
312
+ _print_config_panel(config_file, config_text)
313
+ return config_file
314
+
315
+
316
+ def _write_server_config_linux(tunnel_id, hostname, service, local_cred):
230
317
  etc_cred = ETC_DIR / f"{tunnel_id}.json"
231
318
  config_file = ETC_DIR / "config.yml"
232
- config_text = build_config_yaml(tunnel_id, hostname, ssh_port, str(etc_cred))
319
+ config_text = build_config_yaml(tunnel_id, hostname, service, str(etc_cred))
233
320
 
234
321
  run_command(sudo_prefix() + ["mkdir", "-p", str(ETC_DIR)])
235
322
  if config_file.exists():
@@ -237,15 +324,7 @@ def write_server_config(tunnel_id, hostname, ssh_port):
237
324
  info(f"Backed up existing config to {config_file}.bak")
238
325
  run_command(sudo_prefix() + ["cp", str(local_cred), str(etc_cred)])
239
326
  run_command(sudo_prefix() + ["tee", str(config_file)], capture=True, input_text=config_text)
240
- success(f"Wrote {config_file}")
241
- console.print(
242
- Panel(
243
- Syntax(config_text.rstrip(), "yaml", theme="ansi_dark", background_color="default"),
244
- title=str(config_file),
245
- border_style="step",
246
- expand=False,
247
- )
248
- )
327
+ _print_config_panel(config_file, config_text)
249
328
  return config_file
250
329
 
251
330
 
@@ -277,6 +356,29 @@ def route_dns(name, hostname):
277
356
 
278
357
 
279
358
  def check_sshd(ssh_port):
359
+ if IS_MACOS:
360
+ return _check_sshd_macos(ssh_port)
361
+ return _check_sshd_linux(ssh_port)
362
+
363
+
364
+ def _check_sshd_macos(ssh_port):
365
+ if _port_listening(ssh_port):
366
+ info(f"SSH server is listening on localhost:{ssh_port}.")
367
+ return True
368
+ warning(f"No SSH server is listening on localhost:{ssh_port}.")
369
+ info("On macOS this is the 'Remote Login' service.")
370
+ info("Enable it in System Settings > General > Sharing > Remote Login, or run:")
371
+ console.print(" [cmd]sudo systemsetup -setremotelogin on[/cmd]")
372
+ if Confirm.ask("[accent]Enable Remote Login now (needs sudo)?[/accent]", default=True):
373
+ run_command(sudo_prefix() + ["systemsetup", "-setremotelogin", "on"], check=False)
374
+ if _port_listening(ssh_port):
375
+ success("Remote Login enabled.")
376
+ return True
377
+ warning("Continuing without sshd. The tunnel will not be usable for SSH until it runs.")
378
+ return False
379
+
380
+
381
+ def _check_sshd_linux(ssh_port):
280
382
  if Path("/usr/sbin/sshd").exists() or _has("sshd"):
281
383
  return True
282
384
  warning(f"No SSH server (sshd) found. The tunnel forwards to ssh://localhost:{ssh_port}")
@@ -307,6 +409,37 @@ def cloudflared_bin_for_root():
307
409
 
308
410
 
309
411
  def install_service(config_file):
412
+ if IS_MACOS:
413
+ return _install_service_macos()
414
+ return _install_service_linux(config_file)
415
+
416
+
417
+ def _install_service_macos():
418
+ uid = os.getuid()
419
+ if MAC_PLIST.exists():
420
+ info("cloudflared launch agent already installed. Restarting to apply config.")
421
+ run_command(
422
+ ["launchctl", "kickstart", "-k", f"gui/{uid}/{MAC_LABEL}"],
423
+ check=False,
424
+ status="Restarting launch agent",
425
+ )
426
+ else:
427
+ run_command(
428
+ ["cloudflared", "service", "install"],
429
+ check=False,
430
+ status="Installing cloudflared launch agent",
431
+ )
432
+
433
+ state = mac_service_state()
434
+ if state == "running":
435
+ success("cloudflared launch agent: running")
436
+ else:
437
+ warning(f"cloudflared launch agent: {state or 'unknown'}")
438
+ info("Check logs with: log show --predicate 'process == \"cloudflared\"' --last 5m")
439
+ return state == "running"
440
+
441
+
442
+ def _install_service_linux(config_file):
310
443
  if not Path("/run/systemd/system").exists():
311
444
  warning("systemd not detected. Skipping service install.")
312
445
  info(f"Run the tunnel manually: sudo cloudflared --config {config_file} tunnel run")
@@ -339,7 +472,19 @@ def install_service(config_file):
339
472
  return state == "active"
340
473
 
341
474
 
342
- def print_client_instructions(hostname, ssh_port):
475
+ def manage_hint():
476
+ if IS_MACOS:
477
+ return f"Manage the tunnel here: [cmd]launchctl list {MAC_LABEL}[/cmd] / [cmd]cloudflared tunnel list[/cmd]"
478
+ return (
479
+ "Manage the tunnel here: [cmd]sudo systemctl status cloudflared[/cmd] / [cmd]cloudflared tunnel list[/cmd]"
480
+ )
481
+
482
+
483
+ def run_manually_hint(config_file):
484
+ return f"Run it manually: cloudflared --config {config_file} tunnel run"
485
+
486
+
487
+ def print_ssh_instructions(hostname, ssh_port, config_file, service_installed):
343
488
  user = getpass.getuser()
344
489
  ssh_config = f"Host {hostname}\n User {user}\n ProxyCommand cloudflared access ssh --hostname %h"
345
490
  console.print()
@@ -356,24 +501,51 @@ def print_client_instructions(hostname, ssh_port):
356
501
  )
357
502
  )
358
503
  console.print(Panel(ssh_config, title="client ~/.ssh/config", border_style="step", expand=False))
359
- console.print(
360
- "Manage the tunnel here: [cmd]sudo systemctl status cloudflared[/cmd] / [cmd]cloudflared tunnel list[/cmd]"
361
- )
504
+ if not service_installed:
505
+ console.print(run_manually_hint(config_file))
506
+ console.print(manage_hint())
362
507
  console.print(
363
508
  "Optional hardening: create a self-hosted Access application for "
364
509
  f"[accent]{hostname}[/accent] in the Cloudflare Zero Trust dashboard."
365
510
  )
366
511
 
367
512
 
368
- def run_setup_cloudflare_ssh(hostname, name, ssh_port, no_service):
369
- if platform.system() != "Linux":
370
- error("evo cfssh supports Linux (Ubuntu) only.")
513
+ def print_http_instructions(hostname, http_port, config_file, service_installed):
514
+ console.print()
515
+ console.print(
516
+ Panel(
517
+ "Server side is ready.\n\n"
518
+ f"Local service: [accent]http://localhost:{http_port}[/accent]\n"
519
+ f"Public URL: [accent]https://{hostname}[/accent]\n\n"
520
+ "Open the public URL in a browser (DNS may take a few seconds to propagate).",
521
+ title="cfssh complete (http)",
522
+ border_style="success",
523
+ expand=False,
524
+ )
525
+ )
526
+ if not service_installed:
527
+ console.print(run_manually_hint(config_file))
528
+ console.print(manage_hint())
529
+ console.print(
530
+ "Anything reachable at the public URL is public. Make sure the service has its "
531
+ "own auth, or protect it with a Cloudflare Access application."
532
+ )
533
+
534
+
535
+ def run_setup_cloudflare_tunnel(hostname, name, ssh_port, http_port, no_service):
536
+ if not (IS_MACOS or IS_LINUX):
537
+ error("evo cfssh supports macOS and Linux only.")
371
538
  return
372
539
 
540
+ is_http = http_port is not None
541
+ service_kind = "http" if is_http else "ssh"
542
+ target_port = http_port if is_http else ssh_port
543
+ service = f"http://localhost:{http_port}" if is_http else f"ssh://localhost:{ssh_port}"
544
+
373
545
  existing = detect_local_tunnel()
374
546
  if existing:
375
547
  show_local_tunnel(existing)
376
- existing_host = existing["ssh_hostnames"][0] if existing["ssh_hostnames"] else None
548
+ existing_host = existing["routes"][0][0] if existing["routes"] else None
377
549
  if existing_host:
378
550
  if not hostname:
379
551
  if Confirm.ask(f"[accent]Reuse this tunnel for {existing_host}?[/accent]", default=True):
@@ -390,30 +562,31 @@ def run_setup_cloudflare_ssh(hostname, name, ssh_port, no_service):
390
562
  info("Keeping the existing tunnel. Nothing changed.")
391
563
  return
392
564
 
393
- hostname = hostname or Prompt.ask("[accent]Public hostname for SSH (e.g. dev.example.com)[/accent]")
565
+ hostname = hostname or Prompt.ask("[accent]Public hostname (e.g. dev.example.com)[/accent]")
394
566
  if not hostname:
395
567
  error("Hostname is required.")
396
568
  return
397
569
 
398
570
  name = name or hostname.split(".")[0]
399
- ssh_port = ssh_port or 22
400
571
 
401
572
  summary = Table(show_header=False, box=None, padding=(0, 2, 0, 0))
402
573
  summary.add_row("hostname", f"[accent]{hostname}[/accent]")
403
574
  summary.add_row("tunnel", f"[accent]{name}[/accent]")
404
- summary.add_row("ssh port", f"[accent]{ssh_port}[/accent]")
405
- console.print(Panel(summary, title="Cloudflare SSH tunnel", border_style="step", expand=False))
575
+ summary.add_row("service", f"[accent]{service}[/accent]")
576
+ summary.add_row("platform", f"[accent]{platform.system()}[/accent]")
577
+ console.print(Panel(summary, title="Cloudflare tunnel", border_style="step", expand=False))
406
578
 
407
579
  try:
408
580
  if not install_cloudflared():
409
581
  return
410
- check_sshd(ssh_port)
582
+ if service_kind == "ssh":
583
+ check_sshd(target_port)
411
584
  if not ensure_login():
412
585
  return
413
586
  tunnel_id = ensure_tunnel(name)
414
587
  if not tunnel_id:
415
588
  return
416
- config_file = write_server_config(tunnel_id, hostname, ssh_port)
589
+ config_file = write_server_config(tunnel_id, hostname, service)
417
590
  if not config_file:
418
591
  return
419
592
  run_command(
@@ -422,26 +595,39 @@ def run_setup_cloudflare_ssh(hostname, name, ssh_port, no_service):
422
595
  status="Validating ingress rules",
423
596
  )
424
597
  route_dns(name, hostname)
598
+ service_installed = False
425
599
  if not no_service:
426
- install_service(config_file)
427
- print_client_instructions(hostname, ssh_port)
600
+ service_installed = install_service(config_file)
601
+ if service_kind == "http":
602
+ print_http_instructions(hostname, target_port, config_file, service_installed)
603
+ else:
604
+ print_ssh_instructions(hostname, target_port, config_file, service_installed)
428
605
  except CommandError as exc:
429
606
  error(str(exc))
430
- error("Cloudflare SSH setup did not finish.")
607
+ error("Cloudflare tunnel setup did not finish.")
431
608
 
432
609
 
433
610
  @click.command("cfssh", epilog=EPILOG)
434
- @click.option("-H", "--hostname", help="Public hostname for SSH access, e.g. `dev.example.com`.")
611
+ @click.option("-H", "--hostname", help="Public hostname for the tunnel, e.g. `dev.example.com`.")
435
612
  @click.option("-n", "--name", help="Tunnel name. Defaults to the first label of the hostname.")
436
- @click.option("-P", "--ssh-port", type=int, default=22, show_default=True, help="Local SSH port to forward.")
437
- @click.option("--no-service", is_flag=True, help="Configure only; do not install the systemd service.")
438
- def cfssh(hostname, name, ssh_port, no_service):
439
- """Expose this machine's SSH server through a Cloudflare named tunnel.
440
-
441
- Installs `cloudflared`, creates a named tunnel, writes
442
- `/etc/cloudflared/config.yml` with an `ssh://` ingress rule, routes a
443
- proxied DNS record, and installs the `cloudflared` systemd service. Linux
444
- (Ubuntu) with a Cloudflare-managed domain is required.
613
+ @click.option("-P", "--ssh-port", type=int, default=22, show_default=True, help="Local SSH port to forward (ssh mode).")
614
+ @click.option(
615
+ "--http",
616
+ "http_port",
617
+ type=int,
618
+ default=None,
619
+ metavar="PORT",
620
+ help="Expose a local HTTP service on this port instead of SSH.",
621
+ )
622
+ @click.option("--no-service", is_flag=True, help="Configure only; do not install the launchd/systemd service.")
623
+ def cfssh(hostname, name, ssh_port, http_port, no_service):
624
+ """Expose this machine through a Cloudflare named tunnel.
625
+
626
+ Installs `cloudflared`, creates a named tunnel, writes a `config.yml` with an
627
+ ingress rule, routes a proxied DNS record, and installs the background
628
+ service (launchd on macOS, systemd on Linux). Defaults to forwarding SSH
629
+ (`ssh://localhost:22`); pass `--http PORT` to expose a local web service
630
+ instead. A Cloudflare-managed domain is required.
445
631
  """
446
632
  step("evo cfssh")
447
- run_setup_cloudflare_ssh(hostname, name, ssh_port, no_service)
633
+ run_setup_cloudflare_tunnel(hostname, name, ssh_port, http_port, no_service)