From fc457776157807ac1f252fd446c03727dc49c167 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:14:02 -0700 Subject: [PATCH 1/8] refactor: move Base consumer adapters into Base --- .github/workflows/pylint.yml | 3 +- cli/python/base_clean/engine.py | 2 +- cli/python/base_cli_adapters/__init__.py | 3 + cli/python/base_cli_adapters/config.py | 223 ++++++++++++++++++ cli/python/base_cli_adapters/history.py | 174 ++++++++++++++ cli/python/base_cli_adapters/paths.py | 102 ++++++++ cli/python/base_cli_adapters/runtime.py | 34 +++ cli/python/base_cli_profile.py | 37 ++- cli/python/base_config/engine.py | 3 +- cli/python/base_config/tests/test_engine.py | 4 +- .../base_export_context/tests/test_engine.py | 4 +- cli/python/base_history/engine.py | 22 +- cli/python/base_history/record.py | 14 +- cli/python/base_logs/engine.py | 18 +- cli/python/base_pr_policy/engine.py | 2 +- .../base_pr_policy/tests/test_engine.py | 4 +- cli/python/base_projects/project_discovery.py | 2 +- cli/python/base_projects/tests/test_engine.py | 4 +- cli/python/base_projects/workspace_init.py | 4 +- .../base_projects/workspace_report_common.py | 2 +- cli/python/base_release/tests/test_engine.py | 4 +- cli/python/base_setup/engine.py | 2 +- cli/python/base_setup/tests/test_artifacts.py | 4 +- cli/python/base_trust/engine.py | 2 +- cli/python/base_trust/tests/test_engine.py | 4 +- cli/python/base_trust/trust_store.py | 4 +- 26 files changed, 607 insertions(+), 74 deletions(-) create mode 100644 cli/python/base_cli_adapters/__init__.py create mode 100644 cli/python/base_cli_adapters/config.py create mode 100644 cli/python/base_cli_adapters/history.py create mode 100644 cli/python/base_cli_adapters/paths.py create mode 100644 cli/python/base_cli_adapters/runtime.py diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 9ebf5fb2..f2225c5c 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -25,7 +25,8 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: repository: basefoundry/base-cli - ref: beb99953e5df66dea207a31cea3c00332aaadcad + # Keep the consumer validation paired with base-cli #24 until it merges. + ref: 03c50fb path: .dependencies/base-cli - name: Set up Python ${{ matrix.python-version }} diff --git a/cli/python/base_clean/engine.py b/cli/python/base_clean/engine.py index 594187e5..4672e8a7 100644 --- a/cli/python/base_clean/engine.py +++ b/cli/python/base_clean/engine.py @@ -8,7 +8,7 @@ import base_cli from base_cli_profile import base_cli_app -from base_cli.paths import base_cache_root +from base_cli_adapters.paths import base_cache_root app = base_cli_app(name="base_clean") diff --git a/cli/python/base_cli_adapters/__init__.py b/cli/python/base_cli_adapters/__init__.py new file mode 100644 index 00000000..a7ee8287 --- /dev/null +++ b/cli/python/base_cli_adapters/__init__.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +"""Base-owned adapters for the shared :mod:`base_cli` lifecycle.""" diff --git a/cli/python/base_cli_adapters/config.py b/cli/python/base_cli_adapters/config.py new file mode 100644 index 00000000..e3f05a33 --- /dev/null +++ b/cli/python/base_cli_adapters/config.py @@ -0,0 +1,223 @@ +from __future__ import annotations + +import os +import re +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.ide_schema import parse_ide_extensions +from base_cli.ide_schema import parse_ide_settings + +from base_setup.ide_schema import SUPPORTED_IDES + +from .paths import base_state_root + + +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) + + +def load_user_config(home: Path | None = None) -> dict[str, Any]: + return load_yaml_file(user_config_path(home)) + + +def read_user_config( + home: Path | None = None, + *, + supported_ides: frozenset[str] | None = SUPPORTED_IDES, +) -> UserConfig: + raw = load_user_config(home) + path = user_config_path(home) + return UserConfig( + raw=raw, + workspace=_read_user_workspace_config(path, raw.get("workspace")), + github=_read_user_github_config(path, raw.get("github")), + ide=_read_user_ide_config(path, raw.get("ide"), supported_ides=supported_ides), + ) + + +def _read_user_workspace_config(path: Path, workspace_data: Any) -> UserWorkspaceConfig: + if workspace_data is None: + return UserWorkspaceConfig(root=None) + if not isinstance(workspace_data, dict): + raise ValueError(f"{path}: workspace must be a mapping when provided.") + + allowed_keys = {"root", "manifest", "manifest_source"} + unknown_keys = sorted(set(workspace_data) - allowed_keys) + if unknown_keys: + raise ValueError(f"{path}: workspace has unsupported keys: {', '.join(unknown_keys)}.") + + return UserWorkspaceConfig( + root=_optional_path(path, "workspace.root", workspace_data.get("root")), + manifest=_optional_path(path, "workspace.manifest", workspace_data.get("manifest")), + manifest_source=_optional_non_empty_string( + path, + "workspace.manifest_source", + workspace_data.get("manifest_source"), + ), + ) + + +def _read_user_github_config(path: Path, github_data: Any) -> UserGithubConfig: + if github_data is None: + return UserGithubConfig(default_owner=None, clone_protocol=None) + if not isinstance(github_data, dict): + raise ValueError(f"{path}: github must be a mapping when provided.") + + allowed_keys = {"default_owner", "clone_protocol"} + unknown_keys = sorted(set(github_data) - allowed_keys) + if unknown_keys: + raise ValueError(f"{path}: github has unsupported keys: {', '.join(unknown_keys)}.") + + return UserGithubConfig( + default_owner=_optional_github_owner(path, github_data.get("default_owner")), + clone_protocol=_optional_github_clone_protocol(path, github_data.get("clone_protocol")), + ) + + +def _optional_github_owner(path: Path, value: Any) -> str | None: + owner = _optional_non_empty_string(path, "github.default_owner", value) + if owner is None: + return None + if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9-]*", owner): + raise ValueError( + f"{path}: github.default_owner must start with a letter or digit and contain only " + "letters, digits, and dash." + ) + return owner + + +def _optional_github_clone_protocol(path: Path, value: Any) -> str | None: + protocol = _optional_non_empty_string(path, "github.clone_protocol", value) + if protocol is None: + return None + if protocol not in {"ssh", "https"}: + raise ValueError(f"{path}: github.clone_protocol must be 'ssh' or 'https'.") + return protocol + + +def _optional_path(path: Path, key: str, value: Any) -> Path | None: + if value is None: + return None + candidate = _optional_non_empty_string(path, key, value) + if candidate is None: + return None + + candidate_path = Path(candidate).expanduser() + if not candidate_path.is_absolute(): + raise ValueError(f"{path}: {key} must be an absolute path or start with '~'.") + return candidate_path.resolve(strict=False) + + +def _optional_non_empty_string(path: Path, key: str, value: Any) -> str | None: + if value is None: + return None + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{path}: {key} must be a non-empty string when provided.") + return value.strip() + + +def _read_user_ide_config( + path: Path, + ide_data: Any, + *, + supported_ides: frozenset[str] | None, +) -> UserIdeConfig: + if ide_data is None: + return UserIdeConfig(enabled=None, preferences={}) + if not isinstance(ide_data, dict): + raise ValueError(f"{path}: ide must be a mapping when provided.") + + invalid_keys = sorted(str(key) for key in ide_data if not isinstance(key, str) or not key.strip()) + if invalid_keys: + raise ValueError(f"{path}: ide keys must be non-empty strings.") + + unknown_keys = sorted(set(ide_data) - supported_ides - {"enabled"}) if supported_ides is not None else [] + if unknown_keys: + raise ValueError(f"{path}: unsupported ide keys: {', '.join(unknown_keys)}.") + + enabled = _optional_bool(path, "ide.enabled", ide_data.get("enabled")) + preferences = { + ide_name: _read_user_ide_preference(path, ide_name, ide_data.get(ide_name)) + for ide_name in sorted(set(ide_data) - {"enabled"}) + if ide_name in ide_data + } + return UserIdeConfig(enabled=enabled, preferences=preferences) + + +def _read_user_ide_preference(path: Path, ide_name: str, preference_data: Any) -> UserIdePreference: + if preference_data is None: + preference_data = {} + if not isinstance(preference_data, dict): + raise ValueError(f"{path}: ide.{ide_name} must be a mapping when provided.") + + allowed_keys = {"enabled", "install", "extra_extensions", "settings"} + unknown_keys = sorted(set(preference_data) - allowed_keys) + if unknown_keys: + raise ValueError(f"{path}: ide.{ide_name} has unsupported keys: {', '.join(unknown_keys)}.") + + return UserIdePreference( + enabled=_optional_bool(path, f"ide.{ide_name}.enabled", preference_data.get("enabled")), + install=_optional_bool(path, f"ide.{ide_name}.install", preference_data.get("install")), + extra_extensions=parse_ide_extensions( + f"{path}: ide.{ide_name}.extra_extensions", + preference_data.get("extra_extensions", []), + ), + settings=parse_ide_settings( + f"{path}: ide.{ide_name}.settings", + preference_data.get("settings", {}), + ), + ) + + +def _optional_bool(path: Path, key: str, value: Any) -> bool | None: + if value is None: + return None + if not isinstance(value, bool): + raise ValueError(f"{path}: {key} must be a boolean when provided.") + return value + + +def merge_dicts(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]: + merged = dict(base) + for key, value in override.items(): + if isinstance(value, dict) and isinstance(merged.get(key), dict): + merged[key] = merge_dicts(merged[key], value) + else: + merged[key] = value + return merged + + +def load_config( + project_root: Path | None, + explicit_config: Path | None, + home: Path | None = None, +) -> dict[str, Any]: + root = home or Path.home() + config: dict[str, Any] = {} + config = merge_dicts(config, load_user_config(root)) + if project_root is not None: + config = merge_dicts(config, load_yaml_file(project_root / ".base" / "config.yaml")) + if explicit_config is not None: + config = merge_dicts(config, load_yaml_file(explicit_config)) + + env_config: dict[str, Any] = {} + if "BASE_CLI_ENVIRONMENT" in os.environ: + env_config["environment"] = os.environ["BASE_CLI_ENVIRONMENT"] + if "BASE_CLI_LOG_LEVEL" in os.environ: + env_config["log_level"] = os.environ["BASE_CLI_LOG_LEVEL"] + elif os.environ.get("LOG_DEBUG", "").lower() in ("1", "true"): + env_config["log_level"] = "debug" + if "BASE_CLI_KEEP_TEMP" in os.environ: + env_config["keep_temp"] = os.environ["BASE_CLI_KEEP_TEMP"].lower() == "true" + return merge_dicts(config, env_config) diff --git a/cli/python/base_cli_adapters/history.py b/cli/python/base_cli_adapters/history.py new file mode 100644 index 00000000..c902818c --- /dev/null +++ b/cli/python/base_cli_adapters/history.py @@ -0,0 +1,174 @@ +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any + +import base_cli +from base_cli.context import Context +from base_cli.history import HISTORY_SCOPE_INTERNAL +from base_cli.history import HISTORY_SCOPE_PRIMARY +from base_cli.history import SCHEMA_VERSION +from base_cli.history import compact_home_text +from base_cli.history import compact_optional_path +from base_cli.history import compact_path +from base_cli.history import display_command +from base_cli.history import duration_ms +from base_cli.history import format_timestamp +from base_cli.history import optional_int +from base_cli.history import optional_string +from base_cli.history import parse_finished_history_record_line +from base_cli.history import parse_positive_int +from base_cli.history import redact_history_argv +from base_cli.history import redact_history_text +from base_cli.history import update_run_metadata +from base_cli.history import utc_now +from base_cli.history import write_history_record as write_record + +from .config import load_yaml_file +from .paths import base_cache_root + + +HISTORY_PATH = Path("base") / "history" / "runs.jsonl" + + +def build_finished_record( + context: Context, + argv: list[str], + sensitive_options: set[str], + started_at: Any, + exit_code: int, +) -> dict[str, Any]: + record = base_cli.history.build_finished_record( + context, + argv, + sensitive_options, + started_at, + exit_code, + ) + if "project" not in record: + record["project"] = project_name(context) + version = base_version(context.base_home) + if version: + record["base_version"] = version + return {key: value for key, value in record.items() if value} + + +def project_name(context: Context) -> str | None: + if context.project_name: + return context.project_name + if context.manifest_path is None: + return None + try: + data = load_yaml_file(context.manifest_path) + except (OSError, RuntimeError, ValueError): + return None + project_data = data.get("project") + if not isinstance(project_data, dict): + return None + value = project_data.get("name") + return value if isinstance(value, str) and value else None + + +def base_version(base_home: Path | None) -> str | None: + if base_home is None: + return None + try: + version = (base_home / "VERSION").read_text(encoding="utf-8").splitlines()[0].strip() + except (IndexError, OSError): + return None + return version or None + + +def write_finished_record( + context: Context, + argv: list[str], + sensitive_options: set[str], + started_at: Any, + exit_code: int, +) -> None: + if context.dry_run or context.log_file is None or context.history_scope == HISTORY_SCOPE_INTERNAL: + return + try: + record = build_finished_record(context, argv, sensitive_options, started_at, exit_code) + write_history_record(record) + if context.run_root is not None: + update_run_metadata(context.run_root, record) + except Exception as exc: # pylint: disable=broad-exception-caught + context.log.debug("Unable to write command history record: %s", exc) + + +def write_history_record(record: dict[str, Any]) -> None: + path = base_cache_root() / HISTORY_PATH + write_record(path, record) + + +def runtime_bundle_path() -> Path | None: + value = os.environ.get("BASE_CLI_RUN_ROOT") + if not value: + return None + return Path(value).expanduser().resolve(strict=False) + + +# pylint: disable=too-many-arguments,too-many-positional-arguments +def write_primary_record( + command: str, + argv: list[str], + started_at: Any, + exit_code: int, + run_id: str, + scope: str = HISTORY_SCOPE_PRIMARY, + project: str | None = None, + project_root: str | None = None, + manifest: str | None = None, + log_path: str | None = None, + owner: str = "base", + bundle_path: str | None = None, + *, + raw_command: str = "basectl", +) -> None: + path = base_cache_root() / HISTORY_PATH + base_cli.history.write_primary_record( + path=path, + command=command, + argv=argv, + started_at=started_at, + exit_code=exit_code, + run_id=run_id, + scope=scope, + project=project, + project_root=project_root, + manifest=manifest, + log_path=log_path, + owner=owner, + bundle_path=bundle_path or runtime_bundle_path(), + raw_command=raw_command, + ) + + +__all__ = [ + "HISTORY_PATH", + "HISTORY_SCOPE_INTERNAL", + "HISTORY_SCOPE_PRIMARY", + "SCHEMA_VERSION", + "base_version", + "build_finished_record", + "compact_home_text", + "compact_optional_path", + "compact_path", + "display_command", + "duration_ms", + "format_timestamp", + "optional_int", + "optional_string", + "parse_finished_history_record_line", + "parse_positive_int", + "project_name", + "redact_history_argv", + "redact_history_text", + "runtime_bundle_path", + "utc_now", + "write_finished_record", + "write_history_record", + "write_primary_record", +] diff --git a/cli/python/base_cli_adapters/paths.py b/cli/python/base_cli_adapters/paths.py new file mode 100644 index 00000000..3723115a --- /dev/null +++ b/cli/python/base_cli_adapters/paths.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import hashlib +import os +import sys +from pathlib import Path + +from base_cli.paths import runtime_run_directory_name +from base_cli.paths import runtime_slug + + +def base_state_root(home: Path | None = None) -> Path: + return (home or Path.home()) / ".base.d" + + +def base_cache_root(home: Path | None = None) -> Path: + value = os.environ.get("BASE_CACHE_DIR") + if value: + return Path(value).expanduser() + generic_root = os.environ.get("BASE_CLI_CACHE_DIR") + if generic_root: + return Path(generic_root).expanduser() / "base" + root = home or Path.home() + if sys.platform == "darwin": + return root / "Library" / "Caches" / "base" + return root / ".cache" / "base" + + +def normalize_runtime_owner(value: str | None = None) -> str: + owner = (value or os.environ.get("BASE_CLI_RUNTIME_OWNER") or "base").strip().lower() + if owner not in {"base", "project"}: + raise ValueError("BASE_CLI_RUNTIME_OWNER must be 'base' or 'project'.") + return owner + + +def runtime_project_name(value: str | None = None) -> str | None: + name = (value or os.environ.get("BASE_CLI_PROJECT_NAME") or "").strip() + return name or None + + +def runtime_project_root(value: Path | str | None = None) -> Path | None: + candidate = value or os.environ.get("BASE_CLI_PROJECT_ROOT") + if not candidate: + return None + return Path(candidate).expanduser().resolve() + + +def runtime_owner_root( + cache_root: Path, + owner: str = "base", + project_name: str | None = None, + project_root: Path | None = None, +) -> Path: + normalized_owner = normalize_runtime_owner(owner) + if normalized_owner == "base": + return cache_root / "base" + + name = runtime_slug(project_name or "unnamed") + checkout = checkout_id(project_root) or "unknown" + return cache_root / "projects" / name / checkout + + +def checkout_id(project_root: Path | None) -> str | None: + if project_root is None: + return None + digest = hashlib.sha256(str(project_root.expanduser().resolve()).encode("utf-8")).hexdigest() + return digest[:12] + + +def discover_manifest(start: Path) -> Path | None: + current = start.resolve() + if current.is_file(): + current = current.parent + + while True: + candidate = current / "base_manifest.yaml" + if candidate.is_file(): + return candidate + if current.parent == current: + return None + current = current.parent + + +def resolve_base_home() -> Path | None: + value = os.environ.get("BASE_HOME") + if not value: + return None + return Path(value).expanduser().resolve() + + +__all__ = [ + "base_cache_root", + "base_state_root", + "discover_manifest", + "normalize_runtime_owner", + "resolve_base_home", + "runtime_owner_root", + "runtime_project_name", + "runtime_project_root", + "runtime_run_directory_name", + "runtime_slug", +] diff --git a/cli/python/base_cli_adapters/runtime.py b/cli/python/base_cli_adapters/runtime.py new file mode 100644 index 00000000..8d095d32 --- /dev/null +++ b/cli/python/base_cli_adapters/runtime.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from pathlib import Path + +from base_cli._runtime import RuntimeLayout + +from .paths import runtime_owner_root +from .paths import runtime_run_directory_name + + +def runtime_layout( + cache_root: Path, + cli_name: str, + run_id: str, + *, + owner: str = "base", + project_name: str | None = None, + project_root: Path | None = None, + inherited_run_root: Path | None = None, +) -> RuntimeLayout: + owner_root = runtime_owner_root(cache_root, owner, project_name, project_root) + run_root = inherited_run_root or owner_root / "runs" / runtime_run_directory_name( + run_id, + cli_name, + project_name, + ) + return RuntimeLayout( + owner_root=owner_root, + run_root=run_root, + state_dir=owner_root, + log_dir=run_root / "logs", + cache_dir=owner_root / "cache" / "components" / cli_name, + temp_dir=run_root / "tmp" / cli_name / run_id, + ) diff --git a/cli/python/base_cli_profile.py b/cli/python/base_cli_profile.py index 96fbddff..6d442d8d 100644 --- a/cli/python/base_cli_profile.py +++ b/cli/python/base_cli_profile.py @@ -5,33 +5,27 @@ from typing import Any import base_cli -import base_cli.app as base_cli_app_module -from base_cli._runtime import runtime_layout -from base_cli.config import load_config -from base_cli.config import load_yaml_file -from base_cli.config import read_user_config -from base_cli.history import HISTORY_SCOPE_INTERNAL -from base_cli.history import write_finished_record -from base_cli.paths import base_cache_root -from base_cli.paths import discover_manifest from base_cli.paths import make_run_id -from base_cli.paths import normalize_runtime_owner -from base_cli.paths import resolve_base_home -from base_cli.paths import runtime_project_name -from base_cli.paths import runtime_project_root from base_history.display import display_command as history_display_command from base_setup.ide_schema import SUPPORTED_IDES - -# Keep the legacy patch point available while Base supports released base-cli -# versions that still own this callback in base_cli.app. -base_cli_app_module.write_finished_record = write_finished_record +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.history import HISTORY_SCOPE_INTERNAL +from base_cli_adapters.history import write_finished_record +from base_cli_adapters.paths import base_cache_root +from base_cli_adapters.paths import discover_manifest +from base_cli_adapters.paths import normalize_runtime_owner +from base_cli_adapters.paths import resolve_base_home +from base_cli_adapters.paths import runtime_project_name +from base_cli_adapters.paths import runtime_project_root +from base_cli_adapters.runtime import runtime_layout def base_cli_app(*args: Any, **kwargs: Any) -> Any: - """Construct a Base CLI app with compatibility for older base-cli releases.""" - if hasattr(base_cli, "CliProfile"): - kwargs["profile"] = base_cli_profile() + """Construct a Base CLI app with Base's explicit consumer profile.""" + kwargs["profile"] = base_cli_profile() return base_cli.App(*args, **kwargs) @@ -122,7 +116,8 @@ def _read_user_config() -> base_cli.UserConfig: def _write_finished_record(*args: Any) -> None: - base_cli_app_module.write_finished_record(*args) + """Resolve the adapter at call time so consumers can test the lifecycle.""" + write_finished_record(*args) def _display_command() -> str | None: diff --git a/cli/python/base_config/engine.py b/cli/python/base_config/engine.py index 1bd34fcb..8ed66a11 100644 --- a/cli/python/base_config/engine.py +++ b/cli/python/base_config/engine.py @@ -7,7 +7,8 @@ import base_cli from base_cli_profile import base_cli_app -from base_cli.config import UserConfig, load_user_config, read_user_config, user_config_path +from base_cli.config import UserConfig +from base_cli_adapters.config import load_user_config, read_user_config, user_config_path from base_cli.redaction import REDACTED, is_secret_key, redact_text_value from base_setup.ide_schema import SUPPORTED_IDES diff --git a/cli/python/base_config/tests/test_engine.py b/cli/python/base_config/tests/test_engine.py index 8c606df8..6914316f 100644 --- a/cli/python/base_config/tests/test_engine.py +++ b/cli/python/base_config/tests/test_engine.py @@ -9,8 +9,8 @@ from pathlib import Path from unittest import mock -from base_cli.history import HISTORY_PATH -from base_cli.config import user_config_path +from base_cli_adapters.history import HISTORY_PATH +from base_cli_adapters.config import user_config_path from base_cli.testing import invoke from base_config import engine diff --git a/cli/python/base_export_context/tests/test_engine.py b/cli/python/base_export_context/tests/test_engine.py index 11e4aae2..c2348233 100644 --- a/cli/python/base_export_context/tests/test_engine.py +++ b/cli/python/base_export_context/tests/test_engine.py @@ -9,7 +9,7 @@ from pathlib import Path from unittest import mock -from base_cli.history import build_finished_record +from base_cli_adapters.history import build_finished_record from base_export_context import engine @@ -50,7 +50,7 @@ def test_explicit_project_root_populates_history_project_metadata(self) -> None: with ( mock.patch("base_cli.app.current_working_dir", return_value=outside), mock.patch( - "base_cli.app.write_finished_record", + "base_cli_profile.write_finished_record", side_effect=lambda *args: captured.append(args), ), ): diff --git a/cli/python/base_history/engine.py b/cli/python/base_history/engine.py index 95baeaf5..59ada3f6 100644 --- a/cli/python/base_history/engine.py +++ b/cli/python/base_history/engine.py @@ -13,17 +13,17 @@ from base_cli_profile import base_cli_app from base_cli.command_filters import command_matches from base_cli.command_filters import normalize_command_filters -from base_cli.history import HISTORY_PATH -from base_cli.history import HISTORY_SCOPE_INTERNAL -from base_cli.history import HISTORY_SCOPE_PRIMARY -from base_cli.history import optional_int -from base_cli.history import optional_string -from base_cli.history import parse_finished_history_record_line -from base_cli.history import parse_positive_int -from base_cli.history import redact_history_argv -from base_cli.history import redact_history_text -from base_cli.history import utc_now -from base_cli.paths import base_cache_root +from base_cli_adapters.history import HISTORY_PATH +from base_cli_adapters.history import HISTORY_SCOPE_INTERNAL +from base_cli_adapters.history import HISTORY_SCOPE_PRIMARY +from base_cli_adapters.history import optional_int +from base_cli_adapters.history import optional_string +from base_cli_adapters.history import parse_finished_history_record_line +from base_cli_adapters.history import parse_positive_int +from base_cli_adapters.history import redact_history_argv +from base_cli_adapters.history import redact_history_text +from base_cli_adapters.history import utc_now +from base_cli_adapters.paths import base_cache_root app = base_cli_app(name="base_history", log_to_file=False) diff --git a/cli/python/base_history/record.py b/cli/python/base_history/record.py index bbed7cb9..d807c3b3 100644 --- a/cli/python/base_history/record.py +++ b/cli/python/base_history/record.py @@ -7,13 +7,13 @@ from datetime import datetime, timezone import base_cli -from base_cli.history import HISTORY_PATH -from base_cli.history import optional_int -from base_cli.history import optional_string -from base_cli.history import parse_finished_history_record_line -from base_cli.history import utc_now -from base_cli.paths import base_cache_root -from base_cli.history import write_primary_record +from base_cli_adapters.history import HISTORY_PATH +from base_cli_adapters.history import optional_int +from base_cli_adapters.history import optional_string +from base_cli_adapters.history import parse_finished_history_record_line +from base_cli_adapters.history import utc_now +from base_cli_adapters.paths import base_cache_root +from base_cli_adapters.history import write_primary_record def main(argv: list[str] | None = None) -> int: diff --git a/cli/python/base_logs/engine.py b/cli/python/base_logs/engine.py index ad330cdb..48fca853 100644 --- a/cli/python/base_logs/engine.py +++ b/cli/python/base_logs/engine.py @@ -17,15 +17,15 @@ from base_cli_profile import base_cli_app from base_cli.command_filters import command_matches from base_cli.command_filters import normalize_command_filters -from base_cli.history import HISTORY_PATH -from base_cli.history import compact_path -from base_cli.history import optional_int -from base_cli.history import optional_string -from base_cli.history import parse_finished_history_record_line -from base_cli.history import parse_positive_int -from base_cli.history import redact_history_argv -from base_cli.history import redact_history_text -from base_cli.paths import base_cache_root +from base_cli_adapters.history import HISTORY_PATH +from base_cli_adapters.history import compact_path +from base_cli_adapters.history import optional_int +from base_cli_adapters.history import optional_string +from base_cli_adapters.history import parse_finished_history_record_line +from base_cli_adapters.history import parse_positive_int +from base_cli_adapters.history import redact_history_argv +from base_cli_adapters.history import redact_history_text +from base_cli_adapters.paths import base_cache_root from base_cli.redaction import REDACTED from base_cli.redaction import redact_text_value from base_history.display import display_command diff --git a/cli/python/base_pr_policy/engine.py b/cli/python/base_pr_policy/engine.py index a6b0bfb3..35e51bbb 100644 --- a/cli/python/base_pr_policy/engine.py +++ b/cli/python/base_pr_policy/engine.py @@ -8,7 +8,7 @@ import base_cli from base_cli_profile import base_cli_app -from base_cli.paths import discover_manifest +from base_cli_adapters.paths import discover_manifest from base_setup.github_manifest import GithubPrConfig from base_setup.manifest import read_manifest from base_setup.manifest_loader import ManifestError diff --git a/cli/python/base_pr_policy/tests/test_engine.py b/cli/python/base_pr_policy/tests/test_engine.py index 10a46299..8bce272d 100644 --- a/cli/python/base_pr_policy/tests/test_engine.py +++ b/cli/python/base_pr_policy/tests/test_engine.py @@ -3,7 +3,7 @@ from pathlib import Path from unittest import mock -from base_cli.history import build_finished_record +from base_cli_adapters.history import build_finished_record from base_cli.testing import invoke from base_pr_policy import engine from base_pr_policy.engine import PrPolicyInputs, render_pr_body @@ -23,7 +23,7 @@ def test_explicit_manifest_populates_history_project_metadata(tmp_path) -> None: captured = [] with mock.patch( - "base_cli.app.write_finished_record", + "base_cli_profile.write_finished_record", side_effect=lambda *args: captured.append(args), ): result = invoke( diff --git a/cli/python/base_projects/project_discovery.py b/cli/python/base_projects/project_discovery.py index 17932da1..4ec9cd0c 100644 --- a/cli/python/base_projects/project_discovery.py +++ b/cli/python/base_projects/project_discovery.py @@ -9,7 +9,7 @@ from typing import Any import base_cli -from base_cli.paths import base_cache_root, discover_manifest +from base_cli_adapters.paths import base_cache_root, discover_manifest from base_projects.workspace_scanner import ManifestEntry from base_projects.workspace_scanner import ProjectDiscoveryError from base_projects.workspace_scanner import workspace_manifest_entries diff --git a/cli/python/base_projects/tests/test_engine.py b/cli/python/base_projects/tests/test_engine.py index 0e441742..90ef0625 100644 --- a/cli/python/base_projects/tests/test_engine.py +++ b/cli/python/base_projects/tests/test_engine.py @@ -12,7 +12,7 @@ from unittest import mock from base_cli.command_protocol import loads_records -from base_cli.history import build_finished_record +from base_cli_adapters.history import build_finished_record from base_projects import engine, project_discovery @@ -832,7 +832,7 @@ def test_projects_resolve_populates_history_project_metadata(self) -> None: with ( mock.patch("base_cli.app.current_working_dir", return_value=outside), mock.patch( - "base_cli.app.write_finished_record", + "base_cli_profile.write_finished_record", side_effect=lambda *args: captured.append(args), ), ): diff --git a/cli/python/base_projects/workspace_init.py b/cli/python/base_projects/workspace_init.py index 1e1677e6..ff50833f 100644 --- a/cli/python/base_projects/workspace_init.py +++ b/cli/python/base_projects/workspace_init.py @@ -7,8 +7,8 @@ from urllib.parse import urlparse import base_cli -from base_cli.config import load_user_config -from base_cli.config import user_config_path +from base_cli_adapters.config import load_user_config +from base_cli_adapters.config import user_config_path from base_projects.command_helpers import ProjectCommandError as ProjectRunnerError from base_projects.command_helpers import ProjectUsageError from base_projects.command_helpers import github_repo_spec diff --git a/cli/python/base_projects/workspace_report_common.py b/cli/python/base_projects/workspace_report_common.py index 1ba099b8..04a9834e 100644 --- a/cli/python/base_projects/workspace_report_common.py +++ b/cli/python/base_projects/workspace_report_common.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import Any -from base_cli.paths import base_state_root +from base_cli_adapters.paths import base_state_root from base_projects.workspace_manifest import WorkspaceManifestRepo from base_setup.manifest_model import BaseManifest from base_setup.project_routing import route_for_manifest diff --git a/cli/python/base_release/tests/test_engine.py b/cli/python/base_release/tests/test_engine.py index 11973415..5e4e0f09 100644 --- a/cli/python/base_release/tests/test_engine.py +++ b/cli/python/base_release/tests/test_engine.py @@ -12,7 +12,7 @@ from pathlib import Path from unittest import mock -from base_cli.history import build_finished_record +from base_cli_adapters.history import build_finished_record from base_release import release_publish from base_release import release_readiness from base_release.engine import ReleaseError @@ -213,7 +213,7 @@ def test_explicit_manifest_populates_history_project_metadata(self) -> None: with ( mock.patch("base_cli.app.current_working_dir", return_value=outside), mock.patch( - "base_cli.app.write_finished_record", + "base_cli_profile.write_finished_record", side_effect=lambda *args: captured.append(args), ), ): diff --git a/cli/python/base_setup/engine.py b/cli/python/base_setup/engine.py index 763639ed..27f70780 100644 --- a/cli/python/base_setup/engine.py +++ b/cli/python/base_setup/engine.py @@ -8,7 +8,7 @@ import base_cli from base_cli_profile import base_cli_app from base_cli.config import UserConfig -from base_cli.paths import discover_manifest +from base_cli_adapters.paths import discover_manifest from base_devcontainer.export import DevcontainerExportError from base_devcontainer.export import build_devcontainer_export from base_devcontainer.export import dumps_export_json diff --git a/cli/python/base_setup/tests/test_artifacts.py b/cli/python/base_setup/tests/test_artifacts.py index 787bbe7e..5947c9bd 100644 --- a/cli/python/base_setup/tests/test_artifacts.py +++ b/cli/python/base_setup/tests/test_artifacts.py @@ -13,7 +13,7 @@ from pathlib import Path from unittest import mock -from base_cli.history import build_finished_record +from base_cli_adapters.history import build_finished_record from base_setup import artifacts, process, python_artifacts from base_setup.artifacts import merge_artifacts from base_setup.errors import ArtifactError @@ -913,7 +913,7 @@ def test_explicit_manifest_populates_history_project_metadata(self) -> None: with ( mock.patch("base_cli.app.current_working_dir", return_value=outside), mock.patch( - "base_cli.app.write_finished_record", + "base_cli_profile.write_finished_record", side_effect=lambda *args: captured.append(args), ), ): diff --git a/cli/python/base_trust/engine.py b/cli/python/base_trust/engine.py index a4d5810f..6c9a562a 100644 --- a/cli/python/base_trust/engine.py +++ b/cli/python/base_trust/engine.py @@ -7,7 +7,7 @@ import base_cli from base_cli_profile import base_cli_app -from base_cli.history import base_version as read_base_version +from base_cli_adapters.history import base_version as read_base_version from base_projects import engine as project_engine from base_projects.project_discovery import Project from base_projects.project_discovery import discover_projects_cached diff --git a/cli/python/base_trust/tests/test_engine.py b/cli/python/base_trust/tests/test_engine.py index 739bcb30..6f6265a2 100644 --- a/cli/python/base_trust/tests/test_engine.py +++ b/cli/python/base_trust/tests/test_engine.py @@ -11,7 +11,7 @@ from pathlib import Path from unittest import mock -from base_cli.history import build_finished_record +from base_cli_adapters.history import build_finished_record from base_cli.testing import invoke @@ -109,7 +109,7 @@ def test_require_explicit_manifest_populates_history_project_metadata(self) -> N captured: list[tuple[object, ...]] = [] with mock.patch( - "base_cli.app.write_finished_record", + "base_cli_profile.write_finished_record", side_effect=lambda *args: captured.append(args), ): result = invoke( diff --git a/cli/python/base_trust/trust_store.py b/cli/python/base_trust/trust_store.py index a30b506e..f00e8e2e 100644 --- a/cli/python/base_trust/trust_store.py +++ b/cli/python/base_trust/trust_store.py @@ -7,8 +7,8 @@ from pathlib import Path from typing import Any -from base_cli.history import format_timestamp, utc_now -from base_cli.paths import base_state_root +from base_cli_adapters.history import format_timestamp, utc_now +from base_cli_adapters.paths import base_state_root from base_setup.git_commands import run_git from base_setup.git_remote_parse import parse_origin_remote from base_setup.manifest import read_manifest From e41df49c66c288fdbf21611d998b117635a6f342 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:17:03 -0700 Subject: [PATCH 2/8] ci: pin Base validation to base-cli PR 25 --- .github/workflows/pylint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index f2225c5c..0bcc6b64 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -25,8 +25,8 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: repository: basefoundry/base-cli - # Keep the consumer validation paired with base-cli #24 until it merges. - ref: 03c50fb + # Keep the consumer validation paired with base-cli #25 until it merges. + ref: 03c50fb65cb0a52d5e2f58e208d2fd8c4dee5130 path: .dependencies/base-cli - name: Set up Python ${{ matrix.python-version }} From 3293c3c9ae5be8fb5204a2d9a98acb4f98393331 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:20:24 -0700 Subject: [PATCH 3/8] fix: satisfy Base adapter lint checks --- cli/python/base_cli_adapters/__init__.py | 4 ++-- cli/python/base_cli_adapters/runtime.py | 1 + cli/python/base_config/engine.py | 4 ++-- cli/python/base_logs/engine.py | 6 +++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/cli/python/base_cli_adapters/__init__.py b/cli/python/base_cli_adapters/__init__.py index a7ee8287..ed358b2f 100644 --- a/cli/python/base_cli_adapters/__init__.py +++ b/cli/python/base_cli_adapters/__init__.py @@ -1,3 +1,3 @@ -from __future__ import annotations - """Base-owned adapters for the shared :mod:`base_cli` lifecycle.""" + +from __future__ import annotations diff --git a/cli/python/base_cli_adapters/runtime.py b/cli/python/base_cli_adapters/runtime.py index 8d095d32..289e1b8c 100644 --- a/cli/python/base_cli_adapters/runtime.py +++ b/cli/python/base_cli_adapters/runtime.py @@ -8,6 +8,7 @@ from .paths import runtime_run_directory_name +# pylint: disable=too-many-arguments def runtime_layout( cache_root: Path, cli_name: str, diff --git a/cli/python/base_config/engine.py b/cli/python/base_config/engine.py index 8ed66a11..62cfc39a 100644 --- a/cli/python/base_config/engine.py +++ b/cli/python/base_config/engine.py @@ -6,10 +6,10 @@ from typing import Any import base_cli -from base_cli_profile import base_cli_app from base_cli.config import UserConfig -from base_cli_adapters.config import load_user_config, read_user_config, user_config_path 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_profile import base_cli_app from base_setup.ide_schema import SUPPORTED_IDES diff --git a/cli/python/base_logs/engine.py b/cli/python/base_logs/engine.py index 48fca853..e0bfa187 100644 --- a/cli/python/base_logs/engine.py +++ b/cli/python/base_logs/engine.py @@ -14,9 +14,10 @@ from typing import Any import base_cli -from base_cli_profile import base_cli_app from base_cli.command_filters import command_matches from base_cli.command_filters import normalize_command_filters +from base_cli.redaction import REDACTED +from base_cli.redaction import redact_text_value from base_cli_adapters.history import HISTORY_PATH from base_cli_adapters.history import compact_path from base_cli_adapters.history import optional_int @@ -26,8 +27,7 @@ from base_cli_adapters.history import redact_history_argv from base_cli_adapters.history import redact_history_text from base_cli_adapters.paths import base_cache_root -from base_cli.redaction import REDACTED -from base_cli.redaction import redact_text_value +from base_cli_profile import base_cli_app from base_history.display import display_command From 312b41592361f1be9ab3d112282bc0fb014c532d Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:24:03 -0700 Subject: [PATCH 4/8] ci: pin Base tests to base-cli PR 25 --- .github/workflows/tests.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 34f064a2..03cff2ff 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,7 +31,8 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: repository: basefoundry/base-cli - ref: beb99953e5df66dea207a31cea3c00332aaadcad + # Keep consumer validation paired with base-cli #25 until it merges. + ref: 03c50fb65cb0a52d5e2f58e208d2fd8c4dee5130 path: .dependencies/base-cli - name: Set up Python @@ -69,7 +70,8 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: repository: basefoundry/base-cli - ref: beb99953e5df66dea207a31cea3c00332aaadcad + # Keep consumer validation paired with base-cli #25 until it merges. + ref: 03c50fb65cb0a52d5e2f58e208d2fd8c4dee5130 path: .dependencies/base-cli - name: Expose standalone Python package checkout as sibling @@ -121,7 +123,8 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: repository: basefoundry/base-cli - ref: beb99953e5df66dea207a31cea3c00332aaadcad + # Keep consumer validation paired with base-cli #25 until it merges. + ref: 03c50fb65cb0a52d5e2f58e208d2fd8c4dee5130 path: .dependencies/base-cli - name: Expose standalone Python package checkout as sibling @@ -263,7 +266,8 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: repository: basefoundry/base-cli - ref: beb99953e5df66dea207a31cea3c00332aaadcad + # Keep consumer validation paired with base-cli #25 until it merges. + ref: 03c50fb65cb0a52d5e2f58e208d2fd8c4dee5130 path: .dependencies/base-cli - name: Expose reusable Bash library checkout as sibling From fc17e3bf3ef22be32cf5acb4ee5ca2d2fee0e38c Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:26:24 -0700 Subject: [PATCH 5/8] ci: use paired base-cli in integration tests --- .github/workflows/tests.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 03cff2ff..12a361bb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -212,6 +212,17 @@ jobs: ref: 2c5ef2c3a9edfbe2cf68d0645be65b920255abff path: .dependencies/base-bash-libs + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + repository: basefoundry/base-cli + # Keep consumer validation paired with base-cli #25 until it merges. + ref: 03c50fb65cb0a52d5e2f58e208d2fd8c4dee5130 + path: .dependencies/base-cli + + - name: Expose standalone Python package checkout as sibling + run: | + ln -s "$GITHUB_WORKSPACE/.dependencies/base-cli" "$GITHUB_WORKSPACE/../base-cli" + - name: Set up Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 with: From ca9db386fa17c491df623b9b7a9853f28526fa27 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:33:11 -0700 Subject: [PATCH 6/8] ci: expose paired base-cli to integration fixtures --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 12a361bb..4d4d9f5c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -57,6 +57,7 @@ jobs: timeout-minutes: 20 env: BASE_BASH_LIBS_DIR: ${{ github.workspace }}/.dependencies/base-bash-libs/lib/bash + BASE_CLI_SOURCE_DIR: ${{ github.workspace }}/.dependencies/base-cli/lib/python steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 @@ -202,6 +203,7 @@ jobs: timeout-minutes: 25 env: BASE_BASH_LIBS_DIR: ${{ github.workspace }}/.dependencies/base-bash-libs/lib/bash + BASE_CLI_SOURCE_DIR: ${{ github.workspace }}/.dependencies/base-cli/lib/python steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 From 1c2f26d029b885580eb1b2d110abd621cd38baf5 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:37:44 -0700 Subject: [PATCH 7/8] ci: expose paired base-cli to source checkout tests --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4d4d9f5c..dcd4f254 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -266,6 +266,7 @@ jobs: timeout-minutes: 35 env: BASE_BASH_LIBS_DIR: ${{ github.workspace }}/.dependencies/base-bash-libs/lib/bash + BASE_CLI_SOURCE_DIR: ${{ github.workspace }}/.dependencies/base-cli/lib/python steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 From ea3e63a4d4fb0435067617e643af2fb49ef0f8aa Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:44:21 -0700 Subject: [PATCH 8/8] test: isolate base-cli runtime provider cases --- tests/base_cli_runtime.bats | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/base_cli_runtime.bats b/tests/base_cli_runtime.bats index 87dd2464..854f95ad 100644 --- a/tests/base_cli_runtime.bats +++ b/tests/base_cli_runtime.bats @@ -14,6 +14,7 @@ setup() { touch "$TEST_TMPDIR/base-cli/lib/python/base_cli/__init__.py" run env \ + -u BASE_CLI_SOURCE_DIR \ BASE_HOME="$TEST_RUNTIME_HOME" \ bash -c ' source "$BASE_HOME/lib/base/base_cli_runtime.sh" @@ -29,6 +30,7 @@ setup() { touch "$TEST_TMPDIR/base-cli/lib/python/base_cli/__init__.py" run env \ + -u BASE_CLI_SOURCE_DIR \ BASE_HOME="$TEST_RUNTIME_HOME" \ bash -c ' base_home="$BASE_HOME" @@ -60,6 +62,7 @@ setup() { @test "base_cli runtime reports pip when no source checkout exists" { run env \ + -u BASE_CLI_SOURCE_DIR \ BASE_HOME="$TEST_RUNTIME_HOME" \ bash -c ' source "$BASE_HOME/lib/base/base_cli_runtime.sh" @@ -76,6 +79,7 @@ setup() { touch "$TEST_RUNTIME_HOME/lib/python/base_cli/__init__.py" run env \ + -u BASE_CLI_SOURCE_DIR \ BASE_HOME="$TEST_RUNTIME_HOME" \ bash -c ' source "$BASE_HOME/lib/base/base_cli_runtime.sh"