From 64cf5c672d4395110b7f055b79775f572a91a6ea Mon Sep 17 00:00:00 2001 From: Anthony Date: Wed, 25 Mar 2026 16:39:26 -0400 Subject: [PATCH 1/2] feat: add configurable PiecesOS auto-launch --- .../command_interface/config_command.py | 27 ++++++++++ src/pieces/config/managers/cli.py | 11 ++++ src/pieces/config/schemas/cli.py | 6 ++- src/pieces/settings.py | 10 +++- tests/config_command_test.py | 52 +++++++++++++++++++ 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 tests/config_command_test.py diff --git a/src/pieces/command_interface/config_command.py b/src/pieces/command_interface/config_command.py index 64661d03..f5e2f43d 100644 --- a/src/pieces/command_interface/config_command.py +++ b/src/pieces/command_interface/config_command.py @@ -38,6 +38,17 @@ def get_examples(self): "pieces config --editor nvim", "Set Neovim as default editor" ).example("pieces config --editor vim", "Set Vim as default editor") + builder.section( + header="PiecesOS Launch:", + command_template="pieces config --auto-launch-pieces-os", + ).example( + "pieces config --auto-launch-pieces-os", + "Enable automatic PiecesOS startup when a command needs it", + ).example( + "pieces config --no-auto-launch-pieces-os", + "Disable automatic PiecesOS startup", + ) + return builder.build() def get_docs(self) -> str: @@ -52,15 +63,31 @@ def add_arguments(self, parser: argparse.ArgumentParser): type=str, help="Set the default code editor", ) + parser.add_argument( + "--auto-launch-pieces-os", + dest="auto_launch_pieces_os", + action=argparse.BooleanOptionalAction, + default=None, + help="Automatically launch PiecesOS when a command requires it", + ) def execute(self, **kwargs) -> int: """Execute the config command.""" editor = kwargs.get("editor") + auto_launch = kwargs.get("auto_launch_pieces_os") if editor: Settings.cli_config.editor = editor Settings.logger.print(f"Editor set to: {editor}") + elif auto_launch is not None: + Settings.cli_config.auto_launch_pieces_os = auto_launch + status = "Enabled" if auto_launch else "Disabled" + Settings.logger.print(f"Auto-launch PiecesOS: {status}") else: Settings.logger.print("Current configuration:") Settings.logger.print(f"Editor: {Settings.cli_config.editor or 'Not set'}") + Settings.logger.print( + "Auto-launch PiecesOS: " + + ("Enabled" if Settings.cli_config.auto_launch_pieces_os else "Disabled") + ) return 0 diff --git a/src/pieces/config/managers/cli.py b/src/pieces/config/managers/cli.py index 7d361324..a6767d52 100644 --- a/src/pieces/config/managers/cli.py +++ b/src/pieces/config/managers/cli.py @@ -43,3 +43,14 @@ def theme(self, value: str) -> None: self.config.theme = value self.save() + @property + def auto_launch_pieces_os(self) -> bool: + """Get whether the CLI should auto-launch PiecesOS.""" + return self.config.auto_launch_pieces_os + + @auto_launch_pieces_os.setter + def auto_launch_pieces_os(self, value: bool) -> None: + """Set whether the CLI should auto-launch PiecesOS.""" + self.config.auto_launch_pieces_os = value + self.save() + diff --git a/src/pieces/config/schemas/cli.py b/src/pieces/config/schemas/cli.py index a89027ab..e5b97647 100644 --- a/src/pieces/config/schemas/cli.py +++ b/src/pieces/config/schemas/cli.py @@ -15,6 +15,10 @@ class CLIConfigSchema(BaseModel): ) editor: Optional[str] = Field(default=None, description="Default editor command") theme: str = Field(default="pieces-dark", description="TUI theme preference") + auto_launch_pieces_os: bool = Field( + default=True, + description="Automatically launch PiecesOS when a CLI command requires it", + ) @field_validator("editor") @classmethod @@ -27,4 +31,4 @@ def validate_editor(cls, v): @field_validator('schema_version', mode='before') @classmethod def validate_semver_field(cls, v): - return validate_semver(v) \ No newline at end of file + return validate_semver(v) diff --git a/src/pieces/settings.py b/src/pieces/settings.py index 5f0e3588..b0021585 100644 --- a/src/pieces/settings.py +++ b/src/pieces/settings.py @@ -68,8 +68,16 @@ def startup(cls, bypass_login=False): os_id = cls.get_os_id() sentry_sdk.set_user({"id": os_id or "unknown"}) else: - if cls.pieces_client.is_pieces_running() or cls.open_pieces_widget(): + if cls.pieces_client.is_pieces_running() or ( + cls.cli_config.auto_launch_pieces_os and cls.open_pieces_widget() + ): return cls.startup(bypass_login) + if not cls.cli_config.auto_launch_pieces_os: + cls.logger.print( + "PiecesOS is required but isn't running.\n" + "Start it manually or enable auto-launch with `pieces config --auto-launch-pieces-os`." + ) + sys.exit(2) if cls.logger.confirm( "Pieces OS is required but wasn’t found or couldn’t be launched.\n" "Do you want to install it now and get started?" diff --git a/tests/config_command_test.py b/tests/config_command_test.py new file mode 100644 index 00000000..75e53f58 --- /dev/null +++ b/tests/config_command_test.py @@ -0,0 +1,52 @@ +import pytest +from unittest.mock import Mock, patch + +from pieces.command_interface.config_command import ConfigCommand +from pieces.settings import Settings + + +REAL_SETTINGS_STARTUP = Settings.startup.__func__ + + +class TestConfigCommand: + @pytest.fixture + def config_command(self): + return ConfigCommand() + + @patch.object(Settings, "logger") + @patch.object(Settings, "cli_config") + def test_execute_sets_auto_launch_pieces_os( + self, mock_cli_config, mock_logger, config_command + ): + result = config_command.execute(auto_launch_pieces_os=False) + + assert result == 0 + assert mock_cli_config.auto_launch_pieces_os is False + mock_logger.print.assert_called_once() + assert "auto-launch" in mock_logger.print.call_args[0][0].lower() + + +class TestSettingsStartup: + @patch("sys.exit", side_effect=SystemExit(2)) + @patch.object(Settings, "logger") + @patch.object(Settings, "cli_config") + @patch.object(Settings, "pieces_client") + @patch.object(Settings, "open_pieces_widget") + def test_startup_skips_launch_when_auto_launch_disabled( + self, + mock_open_pieces_widget, + mock_pieces_client, + mock_cli_config, + mock_logger, + mock_sys_exit, + ): + mock_cli_config.auto_launch_pieces_os = False + mock_pieces_client.is_pieces_running.return_value = False + mock_logger.confirm.return_value = False + + with pytest.raises(SystemExit): + REAL_SETTINGS_STARTUP(Settings, bypass_login=False) + + mock_open_pieces_widget.assert_not_called() + mock_logger.print.assert_called() + assert "enable auto-launch" in mock_logger.print.call_args_list[-1][0][0].lower() From 590be008c3f77856b492d0ff151ef9bf42e52973 Mon Sep 17 00:00:00 2001 From: Anthony Date: Wed, 25 Mar 2026 19:01:25 -0400 Subject: [PATCH 2/2] fix: apply config updates independently --- .../command_interface/config_command.py | 4 +- src/pieces/settings.py | 4 +- tests/config_command_test.py | 42 ++++++++++++++++--- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/pieces/command_interface/config_command.py b/src/pieces/command_interface/config_command.py index f5e2f43d..41fbea49 100644 --- a/src/pieces/command_interface/config_command.py +++ b/src/pieces/command_interface/config_command.py @@ -79,11 +79,11 @@ def execute(self, **kwargs) -> int: if editor: Settings.cli_config.editor = editor Settings.logger.print(f"Editor set to: {editor}") - elif auto_launch is not None: + if auto_launch is not None: Settings.cli_config.auto_launch_pieces_os = auto_launch status = "Enabled" if auto_launch else "Disabled" Settings.logger.print(f"Auto-launch PiecesOS: {status}") - else: + if editor is None and auto_launch is None: Settings.logger.print("Current configuration:") Settings.logger.print(f"Editor: {Settings.cli_config.editor or 'Not set'}") Settings.logger.print( diff --git a/src/pieces/settings.py b/src/pieces/settings.py index b0021585..2506aef6 100644 --- a/src/pieces/settings.py +++ b/src/pieces/settings.py @@ -68,9 +68,7 @@ def startup(cls, bypass_login=False): os_id = cls.get_os_id() sentry_sdk.set_user({"id": os_id or "unknown"}) else: - if cls.pieces_client.is_pieces_running() or ( - cls.cli_config.auto_launch_pieces_os and cls.open_pieces_widget() - ): + if cls.cli_config.auto_launch_pieces_os and cls.open_pieces_widget(): return cls.startup(bypass_login) if not cls.cli_config.auto_launch_pieces_os: cls.logger.print( diff --git a/tests/config_command_test.py b/tests/config_command_test.py index 75e53f58..3248f75b 100644 --- a/tests/config_command_test.py +++ b/tests/config_command_test.py @@ -1,7 +1,9 @@ +import argparse import pytest -from unittest.mock import Mock, patch +from unittest.mock import patch from pieces.command_interface.config_command import ConfigCommand +from pieces.config.managers.cli import CLIManager from pieces.settings import Settings @@ -14,17 +16,45 @@ def config_command(self): return ConfigCommand() @patch.object(Settings, "logger") - @patch.object(Settings, "cli_config") - def test_execute_sets_auto_launch_pieces_os( - self, mock_cli_config, mock_logger, config_command + def test_execute_persists_auto_launch_pieces_os( + self, mock_logger, config_command, tmp_path ): - result = config_command.execute(auto_launch_pieces_os=False) + cli_config = CLIManager(tmp_path / "cli.json") + + with patch.object(Settings, "cli_config", cli_config): + result = config_command.execute(auto_launch_pieces_os=False) assert result == 0 - assert mock_cli_config.auto_launch_pieces_os is False + reloaded_config = CLIManager(tmp_path / "cli.json") + assert reloaded_config.auto_launch_pieces_os is False mock_logger.print.assert_called_once() assert "auto-launch" in mock_logger.print.call_args[0][0].lower() + @patch.object(Settings, "logger") + def test_execute_updates_editor_and_auto_launch_together( + self, mock_logger, config_command, tmp_path + ): + cli_config = CLIManager(tmp_path / "cli.json") + + with patch.object(Settings, "cli_config", cli_config): + result = config_command.execute( + editor="vim", auto_launch_pieces_os=False + ) + + assert result == 0 + reloaded_config = CLIManager(tmp_path / "cli.json") + assert reloaded_config.editor == "vim" + assert reloaded_config.auto_launch_pieces_os is False + assert mock_logger.print.call_count == 2 + + def test_add_arguments_supports_no_auto_launch_flag(self, config_command): + parser = argparse.ArgumentParser() + config_command.add_arguments(parser) + + args = parser.parse_args(["--no-auto-launch-pieces-os"]) + + assert args.auto_launch_pieces_os is False + class TestSettingsStartup: @patch("sys.exit", side_effect=SystemExit(2))