diff --git a/.github/workflows/base-check.yml b/.github/workflows/base-check.yml index 089f4631..4967bc5d 100644 --- a/.github/workflows/base-check.yml +++ b/.github/workflows/base-check.yml @@ -104,6 +104,14 @@ jobs: ref: 2c5ef2c3a9edfbe2cf68d0645be65b920255abff path: .base/base-bash-libs + - name: Checkout base-cli source + if: ${{ inputs.setup-mode == 'source-checkout' }} + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + repository: basefoundry/base-cli + ref: 9f3ed25fc0991e3aa9de8411878805c122f90a11 + path: .base/base-cli + - name: Set up Python if: ${{ inputs.setup-mode == 'source-checkout' }} uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 @@ -134,6 +142,7 @@ jobs: run: | python -m venv "$HOME/.base.d/base/.venv" "$HOME/.base.d/base/.venv/bin/python" -m pip install --upgrade pip + "$HOME/.base.d/base/.venv/bin/python" -m pip install --no-deps "$GITHUB_WORKSPACE/.base/base-cli" "$HOME/.base.d/base/.venv/bin/python" -m pip install -r requirements-dev.txt - name: Run basectl check --ci diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 505f1fef..54622ea1 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -25,6 +25,13 @@ jobs: - name: Checkout code uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - name: Checkout base-cli source + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + repository: basefoundry/base-cli + ref: 9f3ed25fc0991e3aa9de8411878805c122f90a11 + path: .dependencies/base-cli + - name: Set up Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 with: @@ -33,6 +40,7 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip + python -m pip install --no-deps .dependencies/base-cli python -m pip install -r requirements-dev.txt - name: Run lightweight Base validation diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 828bcd22..e7cfe7d2 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: fc3ac1826b62de9c84d40c77b131ea3c5e660f14 + ref: 9f3ed25fc0991e3aa9de8411878805c122f90a11 path: .dependencies/base-cli - name: Set up Python ${{ matrix.python-version }} @@ -36,6 +36,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + python -m pip install --no-deps .dependencies/base-cli python -m pip install -r requirements-dev.txt - name: Run Pylint diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3b0263a7..1be989d1 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: fc3ac1826b62de9c84d40c77b131ea3c5e660f14 + ref: 9f3ed25fc0991e3aa9de8411878805c122f90a11 path: .dependencies/base-cli - name: Set up Python @@ -42,6 +42,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + python -m pip install --no-deps .dependencies/base-cli python -m pip install -r requirements-dev.txt - name: Run Python tests @@ -72,7 +73,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: repository: basefoundry/base-cli - ref: fc3ac1826b62de9c84d40c77b131ea3c5e660f14 + ref: 9f3ed25fc0991e3aa9de8411878805c122f90a11 path: .dependencies/base-cli - name: Expose standalone Python package checkout as sibling @@ -124,7 +125,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: repository: basefoundry/base-cli - ref: fc3ac1826b62de9c84d40c77b131ea3c5e660f14 + ref: 9f3ed25fc0991e3aa9de8411878805c122f90a11 path: .dependencies/base-cli - name: Expose standalone Python package checkout as sibling @@ -216,7 +217,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: repository: basefoundry/base-cli - ref: fc3ac1826b62de9c84d40c77b131ea3c5e660f14 + ref: 9f3ed25fc0991e3aa9de8411878805c122f90a11 path: .dependencies/base-cli - name: Expose standalone Python package checkout as sibling @@ -240,10 +241,12 @@ jobs: run: | python -m venv .integration-venv .integration-venv/bin/python -m pip install --upgrade pip + .integration-venv/bin/python -m pip install --no-deps .dependencies/base-cli .integration-venv/bin/python -m pip install -r requirements-dev.txt mkdir -p "$HOME/.base.d/base" python -m venv "$HOME/.base.d/base/.venv" "$HOME/.base.d/base/.venv/bin/python" -m pip install --upgrade pip + "$HOME/.base.d/base/.venv/bin/python" -m pip install --no-deps .dependencies/base-cli "$HOME/.base.d/base/.venv/bin/python" -m pip install -r requirements-dev.txt sudo apt-get update sudo apt-get install -y bats @@ -278,7 +281,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: repository: basefoundry/base-cli - ref: fc3ac1826b62de9c84d40c77b131ea3c5e660f14 + ref: 9f3ed25fc0991e3aa9de8411878805c122f90a11 path: .dependencies/base-cli - name: Expose reusable Bash library checkout as sibling @@ -317,6 +320,7 @@ jobs: mkdir -p "$HOME/.base.d/base" python3 -m venv "$HOME/.base.d/base/.venv" "$HOME/.base.d/base/.venv/bin/python" -m pip install --upgrade pip + "$HOME/.base.d/base/.venv/bin/python" -m pip install --no-deps .dependencies/base-cli "$HOME/.base.d/base/.venv/bin/python" -m pip install -r requirements-dev.txt - name: Run Ubuntu source-checkout validation @@ -336,6 +340,12 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + repository: basefoundry/base-cli + ref: 9f3ed25fc0991e3aa9de8411878805c122f90a11 + path: .dependencies/base-cli + - name: Set up Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 with: @@ -352,6 +362,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + python -m pip install --no-deps .dependencies/base-cli python -m pip install -r requirements-dev.txt sudo apt-get update sudo apt-get install -y shellcheck @@ -362,7 +373,12 @@ jobs: - name: Run pip-audit run: | - python -m pip_audit --cache-dir "$RUNNER_TEMP/pip-audit" -r requirements-dev.txt + # Start from -r requirements-dev.txt, but omit base-cli because it + # is checked out locally and may not be published during review. + grep -Eiv '^base-cli[[:space:]]*=' requirements-dev.txt \ + > "$RUNNER_TEMP/base-audit-requirements.txt" + python -m pip_audit --cache-dir "$RUNNER_TEMP/pip-audit" \ + -r "$RUNNER_TEMP/base-audit-requirements.txt" - name: Run ShellCheck run: | diff --git a/cli/python/base_cli_adapters/config.py b/cli/python/base_cli_adapters/config.py index 5f1def54..c82241f8 100644 --- a/cli/python/base_cli_adapters/config.py +++ b/cli/python/base_cli_adapters/config.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import Any +from base_cli import ConfigurationError from base_cli.config import load_yaml_file as load_cli_yaml_file from base_setup.ide_schema import parse_ide_extensions from base_setup.ide_schema import parse_ide_settings @@ -67,14 +68,17 @@ def read_user_config( *, 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), - ) + try: + 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), + ) + except ValueError as exc: + raise ConfigurationError(str(exc)) from exc def _read_user_workspace_config(path: Path, workspace_data: Any) -> UserWorkspaceConfig: diff --git a/cli/python/base_cli_adapters/runtime.py b/cli/python/base_cli_adapters/runtime.py index 289e1b8c..7f3586dd 100644 --- a/cli/python/base_cli_adapters/runtime.py +++ b/cli/python/base_cli_adapters/runtime.py @@ -2,7 +2,7 @@ from pathlib import Path -from base_cli._runtime import RuntimeLayout +from base_cli.runtime import RuntimeLayout from .paths import runtime_owner_root from .paths import runtime_run_directory_name diff --git a/cli/python/base_github_projects/project_parser.py b/cli/python/base_github_projects/project_parser.py index c7c7efa3..a6eb7eba 100644 --- a/cli/python/base_github_projects/project_parser.py +++ b/cli/python/base_github_projects/project_parser.py @@ -154,6 +154,9 @@ def parse_project_options( state.allow_cross_repo = True index += 1 continue + if apply_inline_option(state, arg, allow_fields=allow_fields): + index += 1 + continue consumed = apply_spaced_option(state, remaining, index, allow_fields=allow_fields) if consumed: index += consumed @@ -183,6 +186,21 @@ def apply_spaced_option(state: OptionState, remaining: list[str], index: int, *, return OPTION_NOT_CONSUMED +def apply_inline_option(state: OptionState, argument: str, *, allow_fields: bool) -> bool: + """Apply Click-compatible ``--option=value`` syntax to delegated options.""" + if not argument.startswith("--") or "=" not in argument: + return False + + option, value = argument.split("=", 1) + if option in PROJECT_VALUE_OPTIONS: + apply_project_option(state, option, value) + return True + if allow_fields and option in ISSUE_FIELD_OPTIONS: + state.field_values[option[2:]] = value + return True + return False + + def option_value(remaining: list[str], index: int) -> str: option = remaining[index] if index + 1 >= len(remaining): diff --git a/cli/python/base_github_projects/tests/test_engine.py b/cli/python/base_github_projects/tests/test_engine.py index bf5ca799..7bfe4915 100644 --- a/cli/python/base_github_projects/tests/test_engine.py +++ b/cli/python/base_github_projects/tests/test_engine.py @@ -26,19 +26,13 @@ def test_delegated_usage_uses_basectl_gh_project_prefix( assert "base_github_projects" not in captured.err -def test_main_rejects_equals_form_project_options( - capsys: pytest.CaptureFixture[str], - monkeypatch: pytest.MonkeyPatch, - tmp_path: Path, -) -> None: - monkeypatch.setenv("HOME", str(tmp_path)) - monkeypatch.setenv("BASE_CACHE_DIR", str(tmp_path / ".cache" / "base")) - - status = engine.main(["project", "configure", "--project=Base Roadmap", "--dry-run"]) +def test_parse_project_configure_accepts_equals_form_options() -> None: + args = engine.parse_args( + ("project", "configure", "--project=Base Roadmap", "--dry-run"), + ) - captured = capsys.readouterr() - assert status == 2 - assert "Option '--project' uses unsupported equals syntax." in captured.err + assert args.project_title == "Base Roadmap" + assert args.dry_run is True def test_parse_project_configure_arguments() -> None: diff --git a/cli/python/base_history/tests/test_engine.py b/cli/python/base_history/tests/test_engine.py index 2d7c2be5..5f4aade7 100644 --- a/cli/python/base_history/tests/test_engine.py +++ b/cli/python/base_history/tests/test_engine.py @@ -109,6 +109,7 @@ def test_text_output_lists_recent_history_and_missing_log_marker(self) -> None: ) status, stdout, stderr = invoke([], cache_root) + records = engine.recent_history(cache_root) self.assertEqual(status, 0) self.assertEqual(stderr, "") @@ -117,7 +118,8 @@ def test_text_output_lists_recent_history_and_missing_log_marker(self) -> None: self.assertIn("PROJECT", stdout) self.assertIn("check", stdout) self.assertIn("error", stdout) - self.assertIn("missing", stdout) + self.assertEqual(len(records), 1) + self.assertTrue(engine.display_log_path(records[0]).endswith(" (missing)")) def test_text_table_expands_columns_for_long_command_and_project(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: diff --git a/cli/python/base_pr_policy/tests/test_engine.py b/cli/python/base_pr_policy/tests/test_engine.py index 8bce272d..287b708a 100644 --- a/cli/python/base_pr_policy/tests/test_engine.py +++ b/cli/python/base_pr_policy/tests/test_engine.py @@ -42,12 +42,15 @@ def test_explicit_manifest_populates_history_project_metadata(tmp_path) -> None: assert record["manifest"] == str(manifest_path.resolve()) -def test_main_rejects_equals_form_options(capsys) -> None: +def test_main_accepts_equals_form_options(capsys, monkeypatch, tmp_path) -> None: + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setenv("BASE_CACHE_DIR", str(tmp_path / ".cache" / "base")) + status = engine.main(["body", "--issue=403"]) captured = capsys.readouterr() - assert status == 2 - assert "Option '--issue' uses unsupported equals syntax." in captured.err + assert status == 0 + assert "Fixes #403" in captured.out def test_render_pr_body_uses_default_label_and_path_sections() -> None: diff --git a/cli/python/base_projects/tests/test_engine.py b/cli/python/base_projects/tests/test_engine.py index 63eac1ce..0ff8aae3 100644 --- a/cli/python/base_projects/tests/test_engine.py +++ b/cli/python/base_projects/tests/test_engine.py @@ -383,7 +383,7 @@ def test_main_reports_config_errors_without_traceback(self) -> None: user_config="workspace: [not-a-mapping]\n", ) - self.assertEqual(status, 1) + self.assertEqual(status, 2) self.assertIn("workspace must be a mapping", stderr) self.assertNotIn("Traceback", stderr) diff --git a/cli/python/base_trust/tests/test_engine.py b/cli/python/base_trust/tests/test_engine.py index 6f6265a2..7d133f5a 100644 --- a/cli/python/base_trust/tests/test_engine.py +++ b/cli/python/base_trust/tests/test_engine.py @@ -120,7 +120,7 @@ def test_require_explicit_manifest_populates_history_project_metadata(self) -> N env={"BASE_HOME": str(root / "base")}, ) - self.assertEqual(result.exit_code, 0) + self.assertEqual(result.exit_code, 1) self.assertIn("Manifest-declared commands are not allowed", result.stderr) self.assertEqual(len(captured), 1) record = build_finished_record(*captured[0]) diff --git a/docs/base-cli.md b/docs/base-cli.md index a69d7951..d4a36fb6 100644 --- a/docs/base-cli.md +++ b/docs/base-cli.md @@ -99,9 +99,9 @@ if __name__ == "__main__": The command function receives `ctx` as its first argument. Infrastructure is created immediately before command execution and cleaned up afterward. -`base_cli.run_app()` applies Base's command syntax guard before Click parses -arguments: long options with values must use space-separated syntax, such as -`--name Ada`; equals-form values such as `--name=Ada` are rejected. +`base_cli.run_app()` preserves Click's native option syntax, including both +space-separated values such as `--name Ada` and equals-form values such as +`--name=Ada`. ## Package Layout @@ -141,7 +141,6 @@ registration. ```python ctx.cli_name # str ctx.run_id # str -ctx.base_home # Path | None ctx.application_home # Path | None; neutral application-home alias ctx.project_name # selected project name, or None ctx.project_root # Path | None @@ -157,14 +156,20 @@ ctx.log_dir # run_root/logs ctx.cache_dir # owner_root/cache/components/ ctx.temp_dir # run_root/tmp// ctx.log_file # run_root/logs/primary.log, or None when disabled -ctx.config # dict +ctx.config # consumer-owned configuration payload +ctx.framework_config # validated framework lifecycle settings, or None +ctx.config_provenance # configuration source mapping, when provided ctx.user_config # typed user config from ~/.base.d/config.yaml +ctx.application_context # optional consumer application state +ctx.services # optional consumer services ctx.history_display_command # consumer policy for persisted command labels ctx.environment # str ctx.debug # bool ctx.dry_run # bool ctx.keep_temp # bool ctx.quiet # bool +ctx.json_output # bool +ctx.rich # bool; optional Rich integration enabled ctx.log # logging.Logger ``` @@ -368,9 +373,8 @@ Direct `base_cli.App` command packages get: | `--version` | show the CLI version when configured | | `--help` | Click help | -Long option values must use the space-separated form, for example -`--environment prod`. Base rejects `--option=value` before Click parses -arguments. +Long option values accept either Click form, for example `--environment prod` +or `--environment=prod`. These are direct Python package options. Public `basectl` launchers expose `-v` for command-level debug logs and command-specific flags from diff --git a/requirements-dev.txt b/requirements-dev.txt index 4c9318e7..6ae9efce 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,6 @@ pylint==3.3.9 click==8.4.1 -base-cli==0.2.0 +base-cli==0.4.0 PyYAML==6.0.3 pytest==9.0.3 pytest-cov==7.1.0 diff --git a/tests/test_base_cli_docs.py b/tests/test_base_cli_docs.py index 4b4f5ff6..a733edba 100644 --- a/tests/test_base_cli_docs.py +++ b/tests/test_base_cli_docs.py @@ -7,7 +7,13 @@ REPO_ROOT = Path(__file__).resolve().parents[1] BASE_CLI_DOC = REPO_ROOT / "docs" / "base-cli.md" -INTERNAL_CONTEXT_FIELDS = {"cleanup_hooks"} +INTERNAL_CONTEXT_FIELDS = { + "cleanup_hooks", + "_run_metadata_path", + "_owns_temp_dir", + "_owned_temp_identity", + "_owned_temp_descriptor", +} def context_section() -> str: