Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ codealmanac setup --yes --garden-off
codealmanac setup --yes --no-auto-update
```

Setup exits before writing configuration or agent instructions on non-macOS
systems because the production scheduler currently depends on `launchd`.

To uninstall CodeAlmanac-owned local artifacts:

```bash
Expand Down
22 changes: 22 additions & 0 deletions src/codealmanac/app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import platform
from collections.abc import Sequence
from dataclasses import dataclass

Expand Down Expand Up @@ -199,6 +200,9 @@ def create_services(
adapters: AppAdapters,
) -> Services:
repositories = RepositoriesService(RepositoryStore(local_state.database_path))
automation_unavailable_reason = default_scheduler_unavailable_reason(
adapters.scheduler
)
automation = AutomationService(adapters.scheduler or LaunchdSchedulerAdapter())
config_service = ConfigService(
ConfigStore(),
Expand Down Expand Up @@ -248,6 +252,7 @@ def create_services(
or PackageToolUninstaller(package_metadata, package_runner),
config_service,
runner_probe=harnesses,
automation_unavailable_reason=automation_unavailable_reason,
)
runs = RunsService(
repositories,
Expand Down Expand Up @@ -287,6 +292,23 @@ def create_services(
)


def default_scheduler_unavailable_reason(
scheduler: SchedulerAdapter | None,
system_name: str | None = None,
) -> str | None:
"""Describe why the production scheduler cannot run on this platform."""
if scheduler is not None:
return None
detected_system = system_name or platform.system()
if detected_system == "Darwin":
return None
return (
"setup currently supports macOS only because scheduled automation uses "
f"launchd; detected {detected_system or 'an unknown platform'}. "
"No setup changes were made."
)


def create_operation(services: Services) -> OperationRunner:
return OperationRunner(
services.repositories,
Expand Down
5 changes: 5 additions & 0 deletions src/codealmanac/services/setup/service.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from codealmanac.core.errors import ValidationFailed
from codealmanac.services.automation.requests import RemoveAllAutomationRequest
from codealmanac.services.config.models import (
AutomationConfig,
Expand Down Expand Up @@ -36,15 +37,19 @@ def __init__(
package_uninstaller: PackageUninstaller,
config: ConfigService,
runner_probe: RunnerReadinessProbe | None = None,
automation_unavailable_reason: str | None = None,
):
self._instructions = instructions
self._automation_remover = automation_remover
self._global_state = global_state
self._package_uninstaller = package_uninstaller
self._config = config
self._runner_probe = runner_probe
self._automation_unavailable_reason = automation_unavailable_reason

def run(self, request: RunSetupRequest) -> SetupResult:
if self._automation_unavailable_reason is not None:
raise ValidationFailed(self._automation_unavailable_reason)
readiness = require_runner(self._runner_probe, request)
config_update = self.set_config(request)
changes = ()
Expand Down
29 changes: 28 additions & 1 deletion tests/test_setup_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import pytest
from pydantic import ValidationError

from codealmanac.core.errors import ExecutionFailed
from codealmanac.app import default_scheduler_unavailable_reason
from codealmanac.core.errors import ExecutionFailed, ValidationFailed
from codealmanac.integrations.setup.instructions import (
CLAUDE_IMPORT_LINE,
CODEALMANAC_END,
Expand Down Expand Up @@ -380,6 +381,30 @@ def test_setup_without_probe_reports_no_readiness(home: Path):
assert result.runner_readiness is None


def test_setup_rejects_unsupported_platform_before_writing(home: Path):
automation = FakeSetupAutomationManager(home)
service = setup_service(
home,
automation=automation,
automation_unavailable_reason=default_scheduler_unavailable_reason(
None, "Linux"
),
)

with pytest.raises(ValidationFailed, match="detected Linux"):
service.run(RunSetupRequest(targets=(SetupTarget.CODEX,)))

assert automation.applied == []
assert not (home / ".codealmanac/config.toml").exists()
assert not (home / ".codex/AGENTS.md").exists()


def test_custom_scheduler_remains_available_on_non_macos():
scheduler = object()

assert default_scheduler_unavailable_reason(scheduler, "Linux") is None


@pytest.fixture
def home(tmp_path: Path) -> Path:
return tmp_path / "home"
Expand All @@ -390,6 +415,7 @@ def setup_service(
automation: "FakeSetupAutomationManager | None" = None,
package_uninstaller: "FakePackageUninstaller | None" = None,
runner_probe: "FakeRunnerProbe | None" = None,
automation_unavailable_reason: str | None = None,
) -> SetupService:
automation_manager = automation or FakeSetupAutomationManager(home)
config = ConfigService(
Expand All @@ -404,6 +430,7 @@ def setup_service(
package_uninstaller or FakePackageUninstaller(skipped_package_result()),
config=config,
runner_probe=runner_probe,
automation_unavailable_reason=automation_unavailable_reason,
)


Expand Down