|
3 | 3 | import os
|
4 | 4 | import sys
|
5 | 5 | import traceback
|
| 6 | +from subprocess import CalledProcessError |
6 | 7 | from typing import Dict, List, Optional, Tuple, TypedDict
|
7 | 8 |
|
8 | 9 | import click
|
@@ -78,6 +79,24 @@ def _run_external(ctx, args):
|
78 | 79 | add_help_option=False,
|
79 | 80 | )
|
80 | 81 |
|
| 82 | + # If user tries 'aws' or 'tf' but tool isn't installed, show install hint |
| 83 | + if cmd_name in ("aws", "tf"): |
| 84 | + from click.decorators import pass_context |
| 85 | + |
| 86 | + def _install_hint(ctx, args): |
| 87 | + raise CLIError( |
| 88 | + f"Tool '{cmd_name}' not installed. " |
| 89 | + f"Run 'localstack tool install {cmd_name}' to install it." |
| 90 | + ) |
| 91 | + |
| 92 | + _install_hint = pass_context(_install_hint) |
| 93 | + return click.Command( |
| 94 | + name=cmd_name, |
| 95 | + params=[click.Argument(["args"], nargs=-1)], |
| 96 | + callback=_install_hint, |
| 97 | + help=f"Install the helper tool '{cmd_name}' via 'localstack tool install {cmd_name}'", |
| 98 | + add_help_option=False, |
| 99 | + ) |
81 | 100 | return None
|
82 | 101 |
|
83 | 102 | def invoke(self, ctx: click.Context):
|
@@ -891,31 +910,111 @@ def localstack_tool() -> None:
|
891 | 910 |
|
892 | 911 |
|
893 | 912 | @localstack_tool.command(name="install", short_help="Install a helper tool")
|
894 |
| -@click.argument("tool", type=click.Choice(["aws"], case_sensitive=False)) |
| 913 | +@click.argument("tool", type=click.Choice(["aws", "tf"], case_sensitive=False)) |
895 | 914 | @publish_invocation
|
896 | 915 | def cmd_tool_install(tool: str) -> None:
|
897 | 916 | """
|
898 | 917 | Install a helper tool. Currently supported:
|
899 | 918 | aws - installs 'awscli-local' via pip.
|
900 | 919 | """
|
901 | 920 | mapping = {
|
902 |
| - "aws": "awscli-local", |
| 921 | + "aws": "https://github.com/localstack/awscli-local.git@localstack-aws-rename", |
| 922 | + "tf": "https://github.com/localstack/terraform-local.git@localstack-tf-rename", |
903 | 923 | }
|
904 |
| - package = mapping.get(tool.lower()) |
905 |
| - if not package: |
| 924 | + repo_url = mapping.get(tool.lower()) |
| 925 | + if not repo_url: |
906 | 926 | raise CLIError(f"Unknown tool: {tool}")
|
907 | 927 |
|
908 |
| - console.rule(f"Installing {package}") |
| 928 | + console.rule(f"Installing {tool}") |
909 | 929 | try:
|
| 930 | + import os |
910 | 931 | import subprocess
|
911 |
| - import sys |
912 |
| - from subprocess import CalledProcessError |
913 | 932 |
|
914 |
| - subprocess.check_call([sys.executable, "-m", "pip", "install", package]) |
915 |
| - console.print(f":heavy_check_mark: Installed {package}") |
916 |
| - except CalledProcessError: |
917 |
| - console.print(f":heavy_multiplication_x: Failed to install {package}", style="bold red") |
918 |
| - sys.exit(1) |
| 933 | + prefix = os.path.expanduser("~/.localstack") |
| 934 | + os.makedirs(prefix, exist_ok=True) |
| 935 | + python_bin = "/Users/silvio/.local/share/mise/installs/python/3.11.12/bin/python" |
| 936 | + subprocess.check_call( |
| 937 | + [ |
| 938 | + python_bin, |
| 939 | + "-m", |
| 940 | + "pip", |
| 941 | + "install", |
| 942 | + "--upgrade", |
| 943 | + "--force-reinstall", |
| 944 | + f"git+{repo_url}", |
| 945 | + "--prefix", |
| 946 | + prefix, |
| 947 | + ] |
| 948 | + ) |
| 949 | + console.print(f":heavy_check_mark: Installed {tool} from {repo_url} into {prefix}") |
| 950 | + except CalledProcessError as e: |
| 951 | + raise CLIError(f"Failed to install {tool} from {repo_url}") from e |
| 952 | + |
| 953 | + |
| 954 | +# Uninstall command for helper tools |
| 955 | +@localstack_tool.command(name="uninstall", short_help="Uninstall a helper tool") |
| 956 | +@click.argument("tool", type=click.Choice(["aws", "tf"], case_sensitive=False)) |
| 957 | +@publish_invocation |
| 958 | +def cmd_tool_uninstall(tool: str) -> None: |
| 959 | + """ |
| 960 | + Uninstall a helper tool by removing its installed files from ~/.localstack. |
| 961 | + Supported: |
| 962 | + aws - uninstalls 'awscli-local' |
| 963 | + tf - uninstalls 'terraform-local' |
| 964 | + """ |
| 965 | + import glob |
| 966 | + import os |
| 967 | + import shutil |
| 968 | + import sys |
| 969 | + |
| 970 | + from localstack.cli.exceptions import CLIError |
| 971 | + |
| 972 | + tool_map = { |
| 973 | + "aws": {"pkg_name": "localstack_awscli", "scripts": ["localstack-aws"]}, |
| 974 | + "tf": {"pkg_name": "localstack_terraform", "scripts": ["localstack-tf"]}, |
| 975 | + } |
| 976 | + info = tool_map.get(tool.lower()) |
| 977 | + if not info: |
| 978 | + raise CLIError(f"Unknown tool: {tool}") |
| 979 | + |
| 980 | + prefix = os.path.expanduser("~/.localstack") |
| 981 | + bin_dir = os.path.join(prefix, "bin") |
| 982 | + lib_dir = os.path.join( |
| 983 | + prefix, "lib", f"python{sys.version_info.major}.{sys.version_info.minor}", "site-packages" |
| 984 | + ) |
| 985 | + |
| 986 | + console.rule(f"Uninstalling {tool}") |
| 987 | + |
| 988 | + # Remove executables |
| 989 | + for script in info["scripts"]: |
| 990 | + path = os.path.join(bin_dir, script) |
| 991 | + if os.path.exists(path): |
| 992 | + try: |
| 993 | + os.remove(path) |
| 994 | + os.remove(path + ".bat") |
| 995 | + console.log(f"Removed script: {path}") |
| 996 | + except OSError: |
| 997 | + console.log(f"Failed to remove script: {path}") |
| 998 | + |
| 999 | + # Remove package directories and metadata |
| 1000 | + patterns = [ |
| 1001 | + f"{info['pkg_name']}*", |
| 1002 | + f"{info['pkg_name'].replace('_', '-')}-*.dist-info*", |
| 1003 | + f"{info['pkg_name']}-*.egg-info*", |
| 1004 | + ] |
| 1005 | + for pattern in patterns: |
| 1006 | + for item in glob.glob(os.path.join(lib_dir, pattern)): |
| 1007 | + try: |
| 1008 | + if os.path.isdir(item): |
| 1009 | + shutil.rmtree(item) |
| 1010 | + console.log(f"Removed directory: {item}") |
| 1011 | + else: |
| 1012 | + os.remove(item) |
| 1013 | + console.log(f"Removed file: {item}") |
| 1014 | + except OSError: |
| 1015 | + console.log(f"Failed to remove: {item}") |
| 1016 | + |
| 1017 | + console.print(f":heavy_check_mark: Uninstalled {tool}") |
919 | 1018 |
|
920 | 1019 |
|
921 | 1020 | @localstack_tool.command(name="ls", short_help="List available helper tools")
|
@@ -948,6 +1047,27 @@ def cmd_tool_ls() -> None:
|
948 | 1047 | console.print("No external LocalStack tools found.")
|
949 | 1048 |
|
950 | 1049 |
|
| 1050 | +# --- new command: tool init --- |
| 1051 | +@localstack_tool.command(name="init", short_help="Initialize environment for helper tools") |
| 1052 | +@publish_invocation |
| 1053 | +def cmd_tool_init() -> None: |
| 1054 | + """ |
| 1055 | + Print shell commands to configure PATH and PYTHONPATH for LocalStack helper tools. |
| 1056 | + """ |
| 1057 | + import os |
| 1058 | + import sys |
| 1059 | + |
| 1060 | + prefix = os.path.expanduser("~/.localstack") |
| 1061 | + bin_dir = os.path.join(prefix, "bin") |
| 1062 | + py_version = f"{sys.version_info.major}.{sys.version_info.minor}" |
| 1063 | + site_packages = os.path.join(prefix, "lib", f"python{py_version}", "site-packages") |
| 1064 | + |
| 1065 | + console.print("To use LocalStack helper tools, add the following to your shell profile:") |
| 1066 | + console.print("") |
| 1067 | + console.print(f' export PATH="{bin_dir}:$PATH"') |
| 1068 | + console.print(f' export PYTHONPATH="{site_packages}:$PYTHONPATH"') |
| 1069 | + |
| 1070 | + |
951 | 1071 | @localstack.command(name="completion", short_help="CLI shell completion")
|
952 | 1072 | @click.pass_context
|
953 | 1073 | @click.argument(
|
|
0 commit comments