diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index dcbf1351..5e4471a8 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: repository: basefoundry/base-cli - ref: 5828f2d829dd9eb7e392455e454768cff7db7aa6 + ref: 8c3854fc69564a264bf66e27a4c046f28ebfcfc5 path: .dependencies/base-cli - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a89b3adc..b334127f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: repository: basefoundry/base-cli - ref: 5828f2d829dd9eb7e392455e454768cff7db7aa6 + ref: 8c3854fc69564a264bf66e27a4c046f28ebfcfc5 path: .dependencies/base-cli - name: Set up Python @@ -69,7 +69,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: repository: basefoundry/base-cli - ref: 5828f2d829dd9eb7e392455e454768cff7db7aa6 + ref: 8c3854fc69564a264bf66e27a4c046f28ebfcfc5 path: .dependencies/base-cli - name: Expose standalone Python package checkout as sibling @@ -121,7 +121,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: repository: basefoundry/base-cli - ref: 5828f2d829dd9eb7e392455e454768cff7db7aa6 + ref: 8c3854fc69564a264bf66e27a4c046f28ebfcfc5 path: .dependencies/base-cli - name: Expose standalone Python package checkout as sibling @@ -213,7 +213,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: repository: basefoundry/base-cli - ref: 5828f2d829dd9eb7e392455e454768cff7db7aa6 + ref: 8c3854fc69564a264bf66e27a4c046f28ebfcfc5 path: .dependencies/base-cli - name: Expose standalone Python package checkout as sibling @@ -275,7 +275,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: repository: basefoundry/base-cli - ref: 5828f2d829dd9eb7e392455e454768cff7db7aa6 + ref: 8c3854fc69564a264bf66e27a4c046f28ebfcfc5 path: .dependencies/base-cli - name: Expose reusable Bash library checkout as sibling diff --git a/cli/python/base_cli_adapters/config.py b/cli/python/base_cli_adapters/config.py index e3f05a33..7798c05e 100644 --- a/cli/python/base_cli_adapters/config.py +++ b/cli/python/base_cli_adapters/config.py @@ -2,15 +2,11 @@ import os import re +from dataclasses import dataclass from pathlib import Path from typing import Any -import base_cli -from base_cli.config import UserConfig -from base_cli.config import UserGithubConfig -from base_cli.config import UserIdeConfig -from base_cli.config import UserIdePreference -from base_cli.config import UserWorkspaceConfig +from base_cli.config import load_yaml_file as load_cli_yaml_file from base_cli.ide_schema import parse_ide_extensions from base_cli.ide_schema import parse_ide_settings @@ -19,12 +15,47 @@ from .paths import base_state_root +@dataclass(frozen=True) +class UserIdePreference: + enabled: bool | None + install: bool | None + extra_extensions: tuple[str, ...] + settings: dict[str, Any] + + +@dataclass(frozen=True) +class UserIdeConfig: + enabled: bool | None + preferences: dict[str, UserIdePreference] + + +@dataclass(frozen=True) +class UserWorkspaceConfig: + root: Path | None + manifest: Path | None = None + manifest_source: str | None = None + + +@dataclass(frozen=True) +class UserGithubConfig: + default_owner: str | None + clone_protocol: str | None + + +@dataclass(frozen=True) +class UserConfig: + raw: dict[str, Any] + ide: UserIdeConfig + workspace: UserWorkspaceConfig = UserWorkspaceConfig(root=None) + github: UserGithubConfig = UserGithubConfig(default_owner=None, clone_protocol=None) + + def user_config_path(home: Path | None = None) -> Path: return base_state_root(home) / "config.yaml" def load_yaml_file(path: Path) -> dict[str, Any]: - return base_cli.config.load_yaml_file(path) + return load_cli_yaml_file(path) def load_user_config(home: Path | None = None) -> dict[str, Any]: diff --git a/cli/python/base_cli_adapters/history.py b/cli/python/base_cli_adapters/history.py index c902818c..a1a7b58a 100644 --- a/cli/python/base_cli_adapters/history.py +++ b/cli/python/base_cli_adapters/history.py @@ -48,7 +48,7 @@ def build_finished_record( ) if "project" not in record: record["project"] = project_name(context) - version = base_version(context.base_home) + version = base_version(context.application_home) if version: record["base_version"] = version return {key: value for key, value in record.items() if value} diff --git a/cli/python/base_cli_profile.py b/cli/python/base_cli_profile.py index 6d442d8d..53017ca1 100644 --- a/cli/python/base_cli_profile.py +++ b/cli/python/base_cli_profile.py @@ -12,6 +12,7 @@ from base_cli_adapters.config import load_config from base_cli_adapters.config import load_yaml_file from base_cli_adapters.config import read_user_config +from base_cli_adapters.config import UserConfig from base_cli_adapters.history import HISTORY_SCOPE_INTERNAL from base_cli_adapters.history import write_finished_record from base_cli_adapters.paths import base_cache_root @@ -105,16 +106,23 @@ def resolve_runtime( explicit, ), resolve_runtime=resolve_runtime, + resolve_workspace_root=_resolve_workspace_root, history_writer=_write_finished_record, display_command=_display_command, history_display_command=history_display_command, ) -def _read_user_config() -> base_cli.UserConfig: +def _read_user_config() -> UserConfig: return read_user_config(supported_ides=SUPPORTED_IDES) +def _resolve_workspace_root(user_config: object | None) -> Path | None: + if isinstance(user_config, UserConfig): + return user_config.workspace.root + return None + + def _write_finished_record(*args: Any) -> None: """Resolve the adapter at call time so consumers can test the lifecycle.""" write_finished_record(*args) diff --git a/cli/python/base_config/engine.py b/cli/python/base_config/engine.py index 62cfc39a..7d044fc1 100644 --- a/cli/python/base_config/engine.py +++ b/cli/python/base_config/engine.py @@ -6,9 +6,9 @@ from typing import Any import base_cli -from base_cli.config import UserConfig from base_cli.redaction import REDACTED, is_secret_key, redact_text_value from base_cli_adapters.config import load_user_config, read_user_config, user_config_path +from base_cli_adapters.config import UserConfig from base_cli_profile import base_cli_app from base_setup.ide_schema import SUPPORTED_IDES diff --git a/cli/python/base_dev/profiles.py b/cli/python/base_dev/profiles.py index 6d69eebc..29e79361 100644 --- a/cli/python/base_dev/profiles.py +++ b/cli/python/base_dev/profiles.py @@ -62,13 +62,13 @@ def read_profile_manifests(ctx: base_cli.Context, profiles: tuple[str, ...]) -> def read_profile_manifest(ctx: base_cli.Context, profile: str) -> BaseManifest: - if ctx.base_home is None: + if ctx.application_home is None: raise ManifestError("BASE_HOME is required to load Base's prerequisite profile manifests.") - return read_manifest(profile_manifest_path(ctx.base_home, profile)) + return read_manifest(profile_manifest_path(ctx.application_home, profile)) def read_dev_manifest(ctx: base_cli.Context) -> BaseManifest: - if ctx.base_home is None: + if ctx.application_home is None: raise ManifestError("BASE_HOME is required to load Base's developer prerequisite manifest.") return read_profile_manifest(ctx, "dev") diff --git a/cli/python/base_projects/engine.py b/cli/python/base_projects/engine.py index 4647950b..25a584ee 100644 --- a/cli/python/base_projects/engine.py +++ b/cli/python/base_projects/engine.py @@ -879,8 +879,8 @@ def bind_project_context(ctx: base_cli.Context, project: Project) -> Project: def find_named_project(ctx: base_cli.Context, project_name: str, workspace: str | None) -> Project | None: - if workspace is None and project_name == "base" and ctx.base_home is not None: - return read_project(ctx.base_home / "base_manifest.yaml") + if workspace is None and project_name == "base" and ctx.application_home is not None: + return read_project(ctx.application_home / "base_manifest.yaml") if workspace is None: active_project = resolve_active_project(project_name) diff --git a/cli/python/base_projects/tests/test_workspace_init.py b/cli/python/base_projects/tests/test_workspace_init.py index 59c2cf73..6f45891e 100644 --- a/cli/python/base_projects/tests/test_workspace_init.py +++ b/cli/python/base_projects/tests/test_workspace_init.py @@ -331,7 +331,7 @@ def test_workspace_init_remote_dry_run_without_local_manifest_stops_after_config def test_resolve_workspace_config_repo_path_uses_explicit_repo_name_guard(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: workspace_root = Path(tmpdir) / "workspace" - ctx = SimpleNamespace(workspace_root=workspace_root, base_home=None) + ctx = SimpleNamespace(workspace_root=workspace_root, application_home=None) source = workspace_init_module.WorkspaceInitSource( display="codeforester/base-workspace", repo_spec="codeforester/base-workspace", diff --git a/cli/python/base_projects/workspace_clone_command.py b/cli/python/base_projects/workspace_clone_command.py index 8db549b2..fcdb2665 100644 --- a/cli/python/base_projects/workspace_clone_command.py +++ b/cli/python/base_projects/workspace_clone_command.py @@ -39,11 +39,11 @@ def workspace_clone_command(ctx: base_cli.Context, options: WorkspaceCloneOption workspace_root = workspace_root.resolve(strict=False) - if ctx.base_home is None: + if ctx.application_home is None: ctx.log.error("BASE_HOME is required to clone workspace repositories.") return base_cli.ExitCode.FAILURE - basectl = ctx.base_home / "bin" / "basectl" + basectl = ctx.application_home / "bin" / "basectl" print(f"Workspace clone: {workspace_root} ({len(manifest.repos)} repositories)") print(f"Workspace manifest: {manifest.path} ({manifest.name})") diff --git a/cli/python/base_projects/workspace_configure.py b/cli/python/base_projects/workspace_configure.py index fc2e8d29..5829d22a 100644 --- a/cli/python/base_projects/workspace_configure.py +++ b/cli/python/base_projects/workspace_configure.py @@ -63,11 +63,11 @@ def workspace_configure_command( *, dry_run: bool, ) -> int: - if ctx.base_home is None: + if ctx.application_home is None: ctx.log.error("BASE_HOME is required to configure workspace repositories.") return base_cli.ExitCode.FAILURE - basectl = ctx.base_home / "bin" / "basectl" + basectl = ctx.application_home / "bin" / "basectl" targets = workspace_configure_targets(workspace_root, workspace_manifest) print_workspace_configure_header(workspace_root, workspace_manifest, len(targets)) diff --git a/cli/python/base_projects/workspace_context.py b/cli/python/base_projects/workspace_context.py index 6f713168..ab65221b 100644 --- a/cli/python/base_projects/workspace_context.py +++ b/cli/python/base_projects/workspace_context.py @@ -13,9 +13,9 @@ def resolve_workspace_root(ctx: base_cli.Context, workspace: str | None) -> Path return Path(workspace).expanduser().resolve() if ctx.workspace_root is not None: return ctx.workspace_root - if ctx.base_home is None: + if ctx.application_home is None: raise ProjectDiscoveryError("BASE_HOME is required to discover workspace projects.") - return ctx.base_home.parent.resolve() + return ctx.application_home.parent.resolve() def effective_workspace_manifest(ctx: base_cli.Context, workspace_manifest: str | None) -> str | None: diff --git a/cli/python/base_projects/workspace_init.py b/cli/python/base_projects/workspace_init.py index ff50833f..d65e03c4 100644 --- a/cli/python/base_projects/workspace_init.py +++ b/cli/python/base_projects/workspace_init.py @@ -179,16 +179,16 @@ def resolve_workspace_init_root(ctx: base_cli.Context, workspace: str | None, co if ctx.workspace_root is not None: return ctx.workspace_root if config_repo is None: - if ctx.base_home is None: + if ctx.application_home is None: raise ProjectDiscoveryError("BASE_HOME is required to resolve the default workspace root.") - return ctx.base_home.parent.resolve(strict=False) + return ctx.application_home.parent.resolve(strict=False) return config_repo.parent.resolve(strict=False) def clone_workspace_config_repo(ctx: base_cli.Context, repo_spec: str, target: Path, *, dry_run: bool) -> None: - if ctx.base_home is None: + if ctx.application_home is None: raise WorkspaceManifestError("BASE_HOME is required to clone the workspace configuration repository.") - basectl = ctx.base_home / "bin" / "basectl" + basectl = ctx.application_home / "bin" / "basectl" command = [str(basectl), "repo", "clone", repo_spec, "--path", str(target)] if dry_run: command.append("--dry-run") diff --git a/cli/python/base_projects/workspace_setup.py b/cli/python/base_projects/workspace_setup.py index a6c1e018..da1888db 100644 --- a/cli/python/base_projects/workspace_setup.py +++ b/cli/python/base_projects/workspace_setup.py @@ -80,11 +80,11 @@ def workspace_setup_command( print_workspace_setup_header(workspace_root, workspace_manifest, len(targets)) counts = WorkspaceSetupCounts() - if not dry_run and ctx.base_home is None: + if not dry_run and ctx.application_home is None: ctx.log.error("BASE_HOME is required to execute workspace setup.") return base_cli.ExitCode.FAILURE - basectl = ctx.base_home / "bin" / "basectl" if ctx.base_home is not None else None + basectl = ctx.application_home / "bin" / "basectl" if ctx.application_home is not None else None if not dry_run and (basectl is None or not basectl.is_file() or not os.access(basectl, os.X_OK)): ctx.log.error("Base CLI '%s' is missing or is not executable.", basectl) return base_cli.ExitCode.FAILURE @@ -241,7 +241,7 @@ def execute_workspace_setup_target( command.append(target.project_name) env = os.environ.copy() - env["BASE_HOME"] = str(ctx.base_home) + env["BASE_HOME"] = str(ctx.application_home) for variable in ("BASE_PROJECT", "BASE_PROJECT_ROOT", "BASE_PROJECT_MANIFEST", "BASE_PROJECT_VENV_DIR"): env.pop(variable, None) diff --git a/cli/python/base_prompt/engine.py b/cli/python/base_prompt/engine.py index 2d313afc..0fe1822f 100644 --- a/cli/python/base_prompt/engine.py +++ b/cli/python/base_prompt/engine.py @@ -105,9 +105,9 @@ def prompt_definition(name: str) -> PromptDefinition: def require_base_home(ctx: base_cli.Context) -> Path: - if ctx.base_home is None: + if ctx.application_home is None: raise PromptError("Base home is unavailable. Run this command through 'basectl prompt'.") - return ctx.base_home + return ctx.application_home def render_prompt(base_home: Path, prompt: PromptDefinition) -> str: diff --git a/cli/python/base_setup/engine.py b/cli/python/base_setup/engine.py index 27f70780..a3c468ef 100644 --- a/cli/python/base_setup/engine.py +++ b/cli/python/base_setup/engine.py @@ -7,7 +7,7 @@ import base_cli from base_cli_profile import base_cli_app -from base_cli.config import UserConfig +from base_cli_adapters.config import UserConfig from base_cli_adapters.paths import discover_manifest from base_devcontainer.export import DevcontainerExportError from base_devcontainer.export import build_devcontainer_export @@ -246,9 +246,9 @@ def validate_project_name(manifest: BaseManifest, expected_project: str | None) def read_default_manifest(ctx: base_cli.Context) -> BaseManifest: - if ctx.base_home is None: + if ctx.application_home is None: raise ManifestError("BASE_HOME is required to load Base's default artifact manifest.") - default_manifest_path = ctx.base_home / "lib" / "base" / "default_manifest.yaml" + default_manifest_path = ctx.application_home / "lib" / "base" / "default_manifest.yaml" return read_manifest(default_manifest_path) diff --git a/cli/python/base_setup/ide.py b/cli/python/base_setup/ide.py index 6a2ae581..e2c6c37d 100644 --- a/cli/python/base_setup/ide.py +++ b/cli/python/base_setup/ide.py @@ -1,7 +1,7 @@ from __future__ import annotations import base_cli -from base_cli.config import UserConfig +from base_cli_adapters.config import UserConfig from .ide_schema import IDE_DEFINITIONS from .ide_schema import IdeDefinition diff --git a/cli/python/base_setup/manifest_checks.py b/cli/python/base_setup/manifest_checks.py index 43d9e121..e929f12c 100644 --- a/cli/python/base_setup/manifest_checks.py +++ b/cli/python/base_setup/manifest_checks.py @@ -2,7 +2,7 @@ import os -from base_cli.config import UserConfig, UserIdeConfig +from base_cli_adapters.config import UserConfig, UserIdeConfig from .artifacts import check_artifact from .artifacts import resolve_artifact_definitions diff --git a/cli/python/base_setup/setup_reconcile.py b/cli/python/base_setup/setup_reconcile.py index 6065ef46..0f9a6176 100644 --- a/cli/python/base_setup/setup_reconcile.py +++ b/cli/python/base_setup/setup_reconcile.py @@ -3,7 +3,7 @@ from dataclasses import replace import base_cli -from base_cli.config import UserConfig +from base_cli_adapters.config import UserConfig from .artifacts import merge_artifacts from .artifacts import ProjectRuntimeConfig diff --git a/cli/python/base_setup/tests/helpers.py b/cli/python/base_setup/tests/helpers.py index 11efce43..0b423ca9 100644 --- a/cli/python/base_setup/tests/helpers.py +++ b/cli/python/base_setup/tests/helpers.py @@ -7,7 +7,7 @@ from pathlib import Path from unittest import mock -from base_cli.config import UserConfig, UserIdeConfig +from base_cli_adapters.config import UserConfig, UserIdeConfig from base_setup.engine import main diff --git a/cli/python/base_setup/tests/test_user_ide_preferences.py b/cli/python/base_setup/tests/test_user_ide_preferences.py index 251109b9..31cb077d 100644 --- a/cli/python/base_setup/tests/test_user_ide_preferences.py +++ b/cli/python/base_setup/tests/test_user_ide_preferences.py @@ -9,7 +9,7 @@ from pathlib import Path from unittest import mock -from base_cli.config import UserConfig, UserIdeConfig, UserIdePreference +from base_cli_adapters.config import UserConfig, UserIdeConfig, UserIdePreference from base_setup import engine, ide from base_setup.github_manifest import GithubConfig, GithubPrConfig from base_setup.manifest import BaseManifest, IdeConfig, read_manifest diff --git a/cli/python/base_trust/engine.py b/cli/python/base_trust/engine.py index 6c9a562a..0e9504fa 100644 --- a/cli/python/base_trust/engine.py +++ b/cli/python/base_trust/engine.py @@ -137,7 +137,7 @@ def allow_command(ctx: base_cli.Context, project: str, workspace: str | None, ma return base_cli.ExitCode.USAGE_ERROR print_identity("Allowing manifest command trust", identity) - ManifestCommandTrustStore().allow(identity, base_version=read_base_version(ctx.base_home)) + ManifestCommandTrustStore().allow(identity, base_version=read_base_version(ctx.application_home)) print(f"Allowed manifest commands for project '{identity.project_name}'.") return base_cli.ExitCode.SUCCESS @@ -220,8 +220,8 @@ def workspace_status_projects(ctx: base_cli.Context, workspace: str | None) -> t if active_project is not None: projects_by_name[active_project.name] = active_project - if ctx.base_home is not None: - base_manifest = ctx.base_home / "base_manifest.yaml" + if ctx.application_home is not None: + base_manifest = ctx.application_home / "base_manifest.yaml" if base_manifest.is_file(): base_project = read_project(base_manifest) projects_by_name[base_project.name] = base_project