"""flow enter — connect to a development instance via SSH.""" import getpass import os import sys from flow.core.config import FlowContext # Default host templates per platform HOST_TEMPLATES = { "orb": ".orb", "utm": ".utm.local", "core": ".core.lan", } def register(subparsers): p = subparsers.add_parser("enter", help="Connect to a development instance via SSH") p.add_argument("target", help="Target: [user@]namespace@platform") p.add_argument("-u", "--user", help="SSH user (overrides target)") p.add_argument("-n", "--namespace", help="Namespace (overrides target)") p.add_argument("-p", "--platform", help="Platform (overrides target)") p.add_argument("-s", "--session", default="default", help="Tmux session name (default: 'default')") p.add_argument("--no-tmux", action="store_true", help="Skip tmux attachment") p.add_argument("-d", "--dry-run", action="store_true", help="Show command without executing") p.set_defaults(handler=run) def _parse_target(target: str): """Parse [user@]namespace@platform into (user, namespace, platform).""" user = None namespace = None platform = None if "@" in target: platform = target.rsplit("@", 1)[1] rest = target.rsplit("@", 1)[0] else: rest = target if "@" in rest: user = rest.rsplit("@", 1)[0] namespace = rest.rsplit("@", 1)[1] else: namespace = rest return user, namespace, platform def _build_destination(user: str, host: str, preserve_host_user: bool = False) -> str: if "@" in host: host_user, host_name = host.rsplit("@", 1) effective_user = host_user if preserve_host_user else (user or host_user) return f"{effective_user}@{host_name}" if not user: return host return f"{user}@{host}" def run(ctx: FlowContext, args): # Warn if already inside an instance if os.environ.get("DF_NAMESPACE") and os.environ.get("DF_PLATFORM"): ns = os.environ["DF_NAMESPACE"] plat = os.environ["DF_PLATFORM"] ctx.console.error( f"Not recommended inside an instance. Currently in: {ns}@{plat}" ) sys.exit(1) user, namespace, platform = _parse_target(args.target) # Apply overrides if args.user: user = args.user if args.namespace: namespace = args.namespace if args.platform: platform = args.platform user_was_explicit = bool(user) if not user: user = os.environ.get("USER") or getpass.getuser() if not namespace: ctx.console.error("Namespace is required in target") sys.exit(1) if not platform: ctx.console.error("Platform is required in target") sys.exit(1) # Resolve SSH host from template or config host_template = HOST_TEMPLATES.get(platform) ssh_identity = None # Check config targets for override for tc in ctx.config.targets: if tc.namespace == namespace and tc.platform == platform: host_template = tc.ssh_host ssh_identity = tc.ssh_identity break if not host_template: ctx.console.error(f"Unknown platform: {platform}") sys.exit(1) ssh_host = host_template.replace("", namespace) destination = _build_destination(user, ssh_host, preserve_host_user=not user_was_explicit) # Build SSH command ssh_cmd = ["ssh", "-tt"] if ssh_identity: ssh_cmd.extend(["-i", os.path.expanduser(ssh_identity)]) ssh_cmd.append(destination) if not args.no_tmux: ssh_cmd.extend([ "tmux", "new-session", "-As", args.session, "-e", f"DF_NAMESPACE={namespace}", "-e", f"DF_PLATFORM={platform}", ]) if args.dry_run: ctx.console.info("Dry run command:") print(" " + " ".join(ssh_cmd)) return os.execvp("ssh", ssh_cmd)