Repository: aiidalab/aiidalab-launch (language: python)

Issue:
Nitpick: Change CLI command `profiles` to `profile`
I noticed that the CLI command to check your profiles is `profiles`:

```console
❯ aiidalab-launch --help
Usage: aiidalab-launch [OPTIONS] COMMAND [ARGS]...

Options:
  -v, --verbose  Increase the output verbosity of the launcher. Use '-vv' or
                 '-vvv' for even more verbose output
  --help         Show this message and exit.

Commands:
  exec      Directly execute a command on an AiiDAlab instance.
  logs      Show the logs of a running AiiDAlab instance.
  profiles  Manage AiiDAlab profiles.
  reset     Reset an AiiDAlab instance.
  start     Start an AiiDAlab instance on this host.
  status    Show AiiDAlab instance status and entry point.
  stop      Stop an AiiDAlab instance on this host.
  version   Show the version of aiidalab-launch.
```

Not unreasonable, but since `verdi` uses `profile`, I vote we switch to use that in the name of consistency. It's backwards incompatible, sure, but `aiidalab-launch` is a top-level tool that I think no scripts should rely on?


Discussion / hints:
While we're at it, `verdi` also shows the default profile by default:

```console
❯ verdi profile show
Report: Profile: dev
PROFILE_UUID: 66c852a2bc4b4047a2eef0c018eadc1a
default_user_email: mbercx@gmail.com
options: {}
process_control:
  backend: rabbitmq
  config:
    broker_host: 127.0.0.1
    broker_password: guest
    broker_port: 5672
    broker_protocol: amqp
    broker_username: guest
    broker_virtual_host: ''
storage:
  backend: core.psql_dos
  config:
    database_engine: postgresql_psycopg2
    database_hostname: localhost
    database_name: aiida-core_dev
    database_password: foobar
    database_port: 5432
    database_username: mbercx
    repository_uri: file:///Users/mbercx/envs/aiida-core/.aiida/repository/dev
test_profile: false
```

Let's do the same for `aiidalab-launch`:

```console
❯ aiidalab-launch profiles show
Usage: aiidalab-launch profiles show [OPTIONS] PROFILE
Try 'aiidalab-launch profiles show --help' for help.

Error: Missing argument 'PROFILE'.
```
I am second on this!

Fix patch:
diff --git a/README.md b/README.md
index 33593fc..68a24af 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ aiidalab-launch status
 ### Profile Management
 
 The tool allows to manage multiple profiles, e.g., with different home directories or ports.
-See `aiidalab-launch profiles --help` for more information.
+See `aiidalab-launch profile --help` for more information.
 
 ### Data Management
 
diff --git a/aiidalab_launch/__main__.py b/aiidalab_launch/__main__.py
index 9cce1fa..0b919ec 100644
--- a/aiidalab_launch/__main__.py
+++ b/aiidalab_launch/__main__.py
@@ -137,12 +137,12 @@ def version():
 
 
 @cli.group()
-def profiles():
+def profile():
     """Manage AiiDAlab profiles."""
     pass
 
 
-@profiles.command("list")
+@profile.command("list")
 @pass_app_state
 def list_profiles(app_state):
     """List all configured AiiDAlab profiles.
@@ -163,7 +163,7 @@ def list_profiles(app_state):
     )
 
 
-@profiles.command("show")
+@profile.command("show")
 @click.argument("profile")
 @pass_app_state
 def show_profile(app_state, profile):
@@ -171,7 +171,7 @@ def show_profile(app_state, profile):
     click.echo(app_state.config.get_profile(profile).dumps(), nl=False)
 
 
-@profiles.command("add")
+@profile.command("add")
 @click.argument("profile")
 @click.option(
     "--port",
@@ -218,7 +218,7 @@ def add_profile(ctx, app_state, port, home_mount, profile):
         ctx.invoke(edit_profile, profile=profile)
 
 
-@profiles.command("remove")
+@profile.command("remove")
 @click.argument("profile")
 @click.option("--yes", is_flag=True, help="Do not ask for confirmation.")
 @click.option("-f", "--force", is_flag=True, help="Proceed, ignoring any warnings.")
@@ -252,7 +252,7 @@ def remove_profile(app_state, profile, yes, force):
             click.echo(f"Removed profile with name '{profile.name}'.")
 
 
-@profiles.command("edit")
+@profile.command("edit")
 @click.argument("profile")
 @pass_app_state
 def edit_profile(app_state, profile):
@@ -269,7 +269,7 @@ def edit_profile(app_state, profile):
     click.echo("No changes.")
 
 
-@profiles.command("set-default")
+@profile.command("set-default")
 @click.argument("profile")
 @pass_app_state
 def set_default_profile(app_state, profile):
@@ -404,7 +404,7 @@ async def _async_start(
         if instance.profile.port and "port is already allocated" in str(error):
             raise click.ClickException(
                 f"Port {instance.profile.port} is already allocated, choose another port "
-                f"for example, by editing the profile: aiidalab-launch profiles edit {instance.profile.name}"
+                f"for example, by editing the profile: aiidalab-launch profile edit {instance.profile.name}"
             )
         raise click.ClickException("Startup failed due to an unexpected error.")
     except asyncio.TimeoutError:


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 4ca99b0..54c4ccc 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -53,39 +53,39 @@ def test_version_verbose_logging():
 
 def test_invalid_profile_name_throws():
     """
-    Arrange/Act: Run `profiles show invalid` subcommand.
+    Arrange/Act: Run `profile show invalid` subcommand.
     Assert:  The command throws an exception due to invalid profile name.
     """
     runner: CliRunner = CliRunner()
     with pytest.raises(ValueError):
         result: Result = runner.invoke(
-            cli.cli, ["profiles", "show", "invalid"], catch_exceptions=False
+            cli.cli, ["profile", "show", "invalid"], catch_exceptions=False
         )
     result: Result = runner.invoke(
         cli.cli,
-        ["profiles", "show", "invalid"],
+        ["profile", "show", "invalid"],
     )
     assert isinstance(result.exception, ValueError)
 
 
 def test_list_profiles():
     runner: CliRunner = CliRunner()
-    result: Result = runner.invoke(cli.cli, ["profiles", "list"])
+    result: Result = runner.invoke(cli.cli, ["profile", "list"])
     assert "default" in result.output.strip()
 
 
 def test_show_profile():
     runner: CliRunner = CliRunner()
-    result: Result = runner.invoke(cli.cli, ["profiles", "show", "default"])
+    result: Result = runner.invoke(cli.cli, ["profile", "show", "default"])
     assert Profile.loads("default", result.output) == Profile()
 
 
 def test_change_default_profile():
     runner: CliRunner = CliRunner()
-    result: Result = runner.invoke(cli.cli, ["profiles", "set-default", "default"])
+    result: Result = runner.invoke(cli.cli, ["profile", "set-default", "default"])
     assert result.exit_code == 0
     result: Result = runner.invoke(
-        cli.cli, ["profiles", "set-default", "does-not-exist"]
+        cli.cli, ["profile", "set-default", "does-not-exist"]
     )
     assert result.exit_code == 1
     assert "does not exist" in result.output
@@ -96,44 +96,44 @@ def test_add_remove_profile():
 
     # Add new-profile
     result: Result = runner.invoke(
-        cli.cli, ["profiles", "add", "new-profile"], input="n\n"
+        cli.cli, ["profile", "add", "new-profile"], input="n\n"
     )
     assert result.exit_code == 0
     assert "Added profile 'new-profile'." in result.output
 
     # Check that new-profile exists
-    result: Result = runner.invoke(cli.cli, ["profiles", "list"])
+    result: Result = runner.invoke(cli.cli, ["profile", "list"])
     assert "new-profile" in result.output
-    result: Result = runner.invoke(cli.cli, ["profiles", "show", "new-profile"])
+    result: Result = runner.invoke(cli.cli, ["profile", "show", "new-profile"])
     assert result.exit_code == 0
 
     # Try add another profile with the same name (should fail)
     result: Result = runner.invoke(
-        cli.cli, ["profiles", "add", "new-profile"], input="n\n"
+        cli.cli, ["profile", "add", "new-profile"], input="n\n"
     )
     assert result.exit_code == 1
     assert "Profile with name 'new-profile' already exists." in result.output
 
     # Try make new profile default
-    result: Result = runner.invoke(cli.cli, ["profiles", "set-default", "new-profile"])
+    result: Result = runner.invoke(cli.cli, ["profile", "set-default", "new-profile"])
     assert result.exit_code == 0
     assert "Set default profile to 'new-profile'." in result.output
     # Reset default profile
-    result: Result = runner.invoke(cli.cli, ["profiles", "set-default", "default"])
+    result: Result = runner.invoke(cli.cli, ["profile", "set-default", "default"])
     assert result.exit_code == 0
     assert "Set default profile to 'default'." in result.output
 
     # Remove new-profile
     result: Result = runner.invoke(
-        cli.cli, ["profiles", "remove", "new-profile"], input="y\n"
+        cli.cli, ["profile", "remove", "new-profile"], input="y\n"
     )
     assert result.exit_code == 0
-    result: Result = runner.invoke(cli.cli, ["profiles", "list"])
+    result: Result = runner.invoke(cli.cli, ["profile", "list"])
     assert "new-profile" not in result.output
 
     # Remove new-profile (again – should fail)
     result: Result = runner.invoke(
-        cli.cli, ["profiles", "remove", "new-profile"], input="y\n"
+        cli.cli, ["profile", "remove", "new-profile"], input="y\n"
     )
     assert result.exit_code == 1
     assert "Profile with name 'new-profile' does not exist." in result.output
@@ -142,7 +142,7 @@ def test_add_remove_profile():
 def test_add_profile_invalid_name():
     runner: CliRunner = CliRunner()
     # underscores are not allowed
-    result: Result = runner.invoke(cli.cli, ["profiles", "add", "new_profile"])
+    result: Result = runner.invoke(cli.cli, ["profile", "add", "new_profile"])
     assert result.exit_code == 1
     assert "Invalid profile name 'new_profile'." in result.output
 
@@ -175,7 +175,7 @@ def test_logs(self):
 
     def test_remove_running_profile(self):
         runner: CliRunner = CliRunner()
-        result: Result = runner.invoke(cli.cli, ["profiles", "remove", "default"])
+        result: Result = runner.invoke(cli.cli, ["profile", "remove", "default"])
         assert result.exit_code == 1
         assert "is still running" in result.output
 
@@ -283,7 +283,7 @@ def test_extra_volumes(
 
         # Check that extra volume is picked up.
         runner: CliRunner = CliRunner()
-        result = runner.invoke(cli.cli, ["profiles", "show", profile.name])
+        result = runner.invoke(cli.cli, ["profile", "show", profile.name])
         assert result.exit_code == 0
         assert extra_volume_name in result.output