Skip to content
Merged
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
16 changes: 14 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,20 @@ and versions are tracked in the repo-root `VERSION` file.

### Changed

- Make `base_cli.App()` use the consumer-neutral profile by default; the
temporary Base compatibility profile is now explicit.
- Make `base_cli.App()` use the consumer-neutral profile by default.
- Move manifest discovery, implicit configuration, owner-aware runtime layout,
and history persistence out of the generic package. Consumers now provide
those policies through an explicit `CliProfile`.

### Migration notes

- The removed implicit Base profile and Base path/config/history helpers are no
longer available from `base_cli`. Existing Base integrations should use the
adapter modules in the Base repository and pass `base_cli_profile()` to
`base_cli.App`.
- `base_cli.history.write_history_record()` and
`base_cli.history.write_primary_record()` now require a consumer-selected
history path; they never choose an application cache location themselves.

### Added

Expand Down
32 changes: 7 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,9 @@ app = base_cli.App(
The generic profile has no manifest filename convention, no product-owned
configuration directory, and no implicit history writer. Applications can
provide those policies through callbacks or build their own profile. The
temporary compatibility profile, `CliProfile.legacy_base()`, preserves the
historical Base behavior only for callers that opt into it explicitly while
Base completes its adapter extraction. See
consumer-owned adapters should supply any product-specific policies. See
[`docs/consumer-profiles.md`](docs/consumer-profiles.md) for the boundary and
migration plan.
migration guidance.

## Public API

Expand Down Expand Up @@ -285,7 +283,6 @@ Important fields include:
- `ctx.cli_name`: normalized CLI name used for state paths and logger names.
- `ctx.run_id`: timestamp plus short random suffix for this invocation.
- `ctx.application_home`: optional application home supplied by the profile.
- `ctx.base_home`: compatibility alias for `ctx.application_home`.
- `ctx.project_root`: project root returned by the profile, when any.
- `ctx.workspace_root`: optional workspace root supplied by user configuration.
- `ctx.manifest_path`: project metadata path returned by the profile, when any.
Expand Down Expand Up @@ -422,11 +419,6 @@ need user files, project files, environment variables, or a merge precedence
must implement those policies in `CliProfile.load_config` and
`CliProfile.load_user_config`.

The legacy Base profile retains its historical `~/.base.d` and `.base`
conventions temporarily; those paths are not part of the generic API. The
Base-specific details remain documented in
[`docs/local-config.md`](docs/local-config.md).

## Project Discovery

The generic profile does not discover projects or assume a manifest filename.
Expand All @@ -436,9 +428,7 @@ from a manifest, workspace, repository metadata, or any other application-owned
source and return a `ProjectInfo` value.

Commands that require a project should validate the profile-provided value
explicitly and return a clear usage error or actionable message. The legacy
Base profile retains upward discovery of `base_manifest.yaml` for existing
callers.
explicitly and return a clear usage error or actionable message.

## Runtime Directories

Expand All @@ -450,9 +440,7 @@ profile does not prescribe a product-wide cache name or cleanup command.

Each invocation is a run bundle containing private (`0600`) `run.json`,
`logs/`, and `tmp/`, while persistent component caches live in the
bundle's cache directory. The legacy Base profile retains the owner-aware
`base/` and `projects/<project>/<checkout-id>` layout for existing
callers.
bundle's cache directory.

Use `ctx.on_cleanup()` for cleanup work that should happen even when helper code
does not own the main command wrapper:
Expand Down Expand Up @@ -487,7 +475,6 @@ def test_command(tmp_path: Path) -> None:
["--name", "Ada"],
home=tmp_path,
cwd=project,
manifest={"project": {"name": "demo"}, "artifacts": []},
)

assert result.exit_code == 0
Expand All @@ -500,11 +487,11 @@ The helper wraps Click's `CliRunner`, sets `HOME` when requested, and supplies
remains process-global: do not use it concurrently with code that changes cwd
outside `invoke()` or from threads spawned by the invoked command. A
generic profile should receive project fixtures through its
`discover_project` callback. The `manifest={...}` convenience is a legacy
compatibility helper for the Base profile.
`discover_project` callback. The helper does not create or interpret any
product-specific manifest fixture.

When `home` is supplied, `invoke()` provides an isolated default cache
environment for tests. Pass `env={"BASE_CACHE_DIR": str(path)}` when a test
environment for tests. Pass `env={"BASE_CLI_CACHE_DIR": str(path)}` when a test
needs an explicit cache location.

## When To Use `base_cli`
Expand All @@ -514,11 +501,6 @@ lifecycle: standard options, logging, redaction, runtime state, cleanup, and
test helpers. Standalone consumers should use `CliProfile.generic()` or
provide an explicit profile with their own project and configuration policies.

The legacy Base profile exists only for compatibility while Base's command
engines migrate to an explicit consumer adapter. Base-specific behavior such as
manifest discovery, `.base` configuration, IDE settings, and command history
should eventually live in that adapter rather than in the generic package.

It is a good fit for:

- project discovery commands
Expand Down
10 changes: 5 additions & 5 deletions docs/cache-ownership-and-layout.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Cache ownership and layout

Runtime state is rooted at `~/Library/Caches/base` on macOS and `~/.cache/base`
elsewhere. Set `BASE_CACHE_DIR` to override the root.
Runtime state is rooted at the cache root supplied to `CliProfile.generic()` or
the platform cache directory. The generic profile places each application in a
sanitized application namespace and does not impose a product-wide cache name.

The `base` owner stores Base control-plane runs directly below `base/`. A
project-owned runtime uses `projects/<project>/<checkout-id>/` so separate
checkouts do not share mutable run state accidentally.
Consumer profiles may choose a different cache root or owner-aware layout when
their application needs stronger isolation between projects or checkouts.

Each invocation has a private run bundle containing:

Expand Down
33 changes: 10 additions & 23 deletions docs/consumer-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,25 +76,17 @@ translate internal entry-point names into user-facing labels. The generic
default only replaces underscores with hyphens; it does not know any product's
command aliases.

## Compatibility profile
## Consumer-owned adapters

`App()` uses `CliProfile.generic()` when no profile is supplied. This keeps the
standalone default consumer-neutral. During the migration, Base and other
existing integrations that still need these conventions can opt into
`CliProfile.legacy_base()` explicitly while they move their adapters out of the
generic package.
standalone default consumer-neutral. A product consumer that needs manifest
discovery, implicit configuration, owner-aware runtime placement, or history
should implement those policies in its own adapter module and pass the resulting
profile to `App`.

The legacy profile contains the current Base conventions, including:

- upward discovery of `base_manifest.yaml`;
- `BASE_HOME`, `BASE_CACHE_DIR`, and Base owner/runtime environment variables;
- `~/.base.d/config.yaml` and project `.base/config.yaml`;
- Base's owner-aware cache and run layout;
- Base history persistence and delegation metadata. Base's command-label policy
is supplied by Base rather than encoded in `base_cli.history`.

These conventions are intentionally isolated behind one profile so they can be
moved into the Base consumer without changing command lifecycle code.
The generic history helpers likewise do not select a product-owned history
path. Consumers resolve that path in their adapter and pass it to
`write_history_record()` or `write_primary_record()`.

## Refactoring boundary

Expand All @@ -108,13 +100,8 @@ The following behaviors should not be added to generic lifecycle modules:
- product-specific command lists or history schema;
- assumptions about a downstream repository's directory layout.

The remaining migration phases are:

1. Move Base discovery, config, runtime, and history adapters into Base.
2. Generalize the remaining context/config types where their names still encode
Base concepts.
3. Remove the compatibility profile and keep `base_cli` focused on the generic
lifecycle.
The next migration step is to generalize the remaining context/config types
whose compatibility names still reflect one historical consumer.

The package rename is deliberately separate from this refactor. Names can be
changed after the dependency boundary is stable.
14 changes: 6 additions & 8 deletions docs/local-config.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
# Local configuration

`base-cli` reads machine-local configuration from `~/.base.d/config.yaml`.
Project configuration is read from `<project>/.base/config.yaml`, and an
explicit `--config` file can provide the final project-specific override.
`base-cli` does not read machine-local or project configuration implicitly.
Standalone applications can accept an explicit `--config` file through the
generic profile, or provide their own `load_user_config` and `load_config`
callbacks for application-owned configuration sources.

The package owns the configuration schema and merge semantics. Users own the
operational choice of whether to back up or synchronize the machine-local file,
using tools such as iCloud, chezmoi, a dotfiles repository, Time Machine, or a
manual copy. The file can contain paths and other machine-specific values and
should not be synchronized blindly across incompatible machines.
The consumer owns the configuration schema, merge semantics, and operational
choice of whether to back up or synchronize its machine-local files.
16 changes: 5 additions & 11 deletions lib/python/base_cli/_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from pathlib import Path

from ._private_files import write_private_json
from .paths import runtime_owner_root, runtime_run_directory_name, runtime_slug
from .paths import runtime_run_directory_name, runtime_slug


@dataclass(frozen=True)
Expand All @@ -28,17 +28,11 @@ def runtime_layout(
cli_name: str,
run_id: str,
*,
owner: str = "base",
namespace: str | None = None,
project_name: str | None = None,
project_root: Path | None = None,
inherited_run_root: Path | None = None,
) -> RuntimeLayout:
owner_root = (
runtime_namespace_root(cache_root, namespace)
if namespace is not None
else runtime_owner_root(cache_root, owner, project_name, project_root)
)
owner_root = runtime_namespace_root(cache_root, namespace or cli_name)
run_root = inherited_run_root or owner_root / "runs" / runtime_run_directory_name(run_id, cli_name, project_name)
state_dir = owner_root
# Every public invocation owns one run bundle and one diagnostic log.
Expand Down Expand Up @@ -130,7 +124,7 @@ def _is_within(path: Path, root: Path) -> bool:

def _runtime_directory_error(path: Path, cache_root: Path, exc: OSError) -> str:
return (
f"Unable to create Base runtime directory '{path}': {exc}. "
f"Check permissions on that directory. If the Base cache root '{cache_root}' is unusable, "
"set BASE_CACHE_DIR to a writable directory."
f"Unable to create runtime directory '{path}': {exc}. "
f"Check permissions on that directory. If the cache root '{cache_root}' is unusable, "
"configure the application's cache root."
)
2 changes: 1 addition & 1 deletion lib/python/base_cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def __init__(
self.max_log_files = max_log_files
# Standalone applications must not inherit a consumer's product
# conventions. Consumers with an existing integration should pass an
# explicit profile, such as Base's temporary legacy adapter.
# Consumers with product-specific policies should pass an explicit profile.
self.profile = profile or CliProfile.generic()
self._click_command = None
self._command_func: Callable[..., Any] | None = None
Expand Down
2 changes: 1 addition & 1 deletion lib/python/base_cli/command_filters.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Shared command-name filter normalization for Base reports."""
"""Shared command-name filter normalization for CLI reports."""

from __future__ import annotations

Expand Down
Loading
Loading