py-wlcli 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py_wlcli-0.8.0.dist-info/METADATA +139 -0
- py_wlcli-0.8.0.dist-info/RECORD +320 -0
- py_wlcli-0.8.0.dist-info/WHEEL +4 -0
- py_wlcli-0.8.0.dist-info/entry_points.txt +2 -0
- py_wlcli-0.8.0.dist-info/licenses/LICENSE +21 -0
- wl_cli/__init__.py +27 -0
- wl_cli/__main__.py +130 -0
- wl_cli/capabilities.py +79 -0
- wl_cli/cc/__init__.py +5 -0
- wl_cli/cc/checks.py +1019 -0
- wl_cli/cc/cli.py +370 -0
- wl_cli/cc/mutate.py +1350 -0
- wl_cli/cli_click.py +156 -0
- wl_cli/commands/__init__.py +84 -0
- wl_cli/commands/base.py +80 -0
- wl_cli/commands/buildcommands/__init__.py +1 -0
- wl_cli/commands/buildcommands/build.py +71 -0
- wl_cli/commands/buildcommands/build_utils.py +47 -0
- wl_cli/commands/buildcommands/builders/__init__.py +18 -0
- wl_cli/commands/buildcommands/builders/cleanup.py +35 -0
- wl_cli/commands/buildcommands/builders/coordinator.py +122 -0
- wl_cli/commands/buildcommands/builders/environment.py +81 -0
- wl_cli/commands/buildcommands/builders/rust_builder.py +39 -0
- wl_cli/commands/buildcommands/builders/stubs.py +58 -0
- wl_cli/commands/buildcommands/dist.py +326 -0
- wl_cli/commands/buildcommands/test.py +89 -0
- wl_cli/commands/clean/__init__.py +1 -0
- wl_cli/commands/clean/build.py +151 -0
- wl_cli/commands/clean/clean.py +294 -0
- wl_cli/commands/clean/clean_utils.py +77 -0
- wl_cli/commands/clean/core.py +236 -0
- wl_cli/commands/clean/dead_code/__init__.py +0 -0
- wl_cli/commands/clean/dead_code/dead_code_command.py +70 -0
- wl_cli/commands/clean/dead_code/detector.py +161 -0
- wl_cli/commands/clean/dead_code/fixer.py +176 -0
- wl_cli/commands/clean/dead_code/reporter.py +295 -0
- wl_cli/commands/clean/dead_code/utils.py +0 -0
- wl_cli/commands/clean/lfs.py +215 -0
- wl_cli/commands/clean/python.py +81 -0
- wl_cli/commands/clean/rust.py +91 -0
- wl_cli/commands/clean/utils/__init__.py +34 -0
- wl_cli/commands/clean/venv.py +91 -0
- wl_cli/commands/cleanup.py +73 -0
- wl_cli/commands/click_base.py +79 -0
- wl_cli/commands/config/__init__.py +1 -0
- wl_cli/commands/config/config.py +291 -0
- wl_cli/commands/debug/__init__.py +6 -0
- wl_cli/commands/debug/debug.py +309 -0
- wl_cli/commands/debug/debug_cache.py +86 -0
- wl_cli/commands/debug/debug_subagent.py +156 -0
- wl_cli/commands/debug/output_formatter.py +165 -0
- wl_cli/commands/debug/source_hasher.py +92 -0
- wl_cli/commands/format/__init__.py +13 -0
- wl_cli/commands/format/format_cache_adapter.py +125 -0
- wl_cli/commands/format/format_command.py +197 -0
- wl_cli/commands/format/format_coordinator.py +191 -0
- wl_cli/commands/format/format_executor.py +470 -0
- wl_cli/commands/format/format_outcome.py +44 -0
- wl_cli/commands/format/format_path_handler.py +1016 -0
- wl_cli/commands/format/format_progress.py +62 -0
- wl_cli/commands/format/format_reporter.py +31 -0
- wl_cli/commands/format/python_formatter.py +506 -0
- wl_cli/commands/format/rust_formatter.py +106 -0
- wl_cli/commands/gate/__init__.py +1 -0
- wl_cli/commands/gate/gate_command.py +862 -0
- wl_cli/commands/gate/preflight.py +445 -0
- wl_cli/commands/git/__init__.py +1 -0
- wl_cli/commands/git/config.py +169 -0
- wl_cli/commands/git/git_facts.py +60 -0
- wl_cli/commands/git/git_flow.py +86 -0
- wl_cli/commands/git/hook.py +112 -0
- wl_cli/commands/git/policy.py +216 -0
- wl_cli/commands/git/push_update.py +82 -0
- wl_cli/commands/initenv/__init__.py +1 -0
- wl_cli/commands/initenv/common/__init__.py +23 -0
- wl_cli/commands/initenv/common/check_git_bash.py +78 -0
- wl_cli/commands/initenv/common/error_handler.py +68 -0
- wl_cli/commands/initenv/common/exceptions.py +52 -0
- wl_cli/commands/initenv/components/__init__.py +4 -0
- wl_cli/commands/initenv/components/cargo_sync.py +240 -0
- wl_cli/commands/initenv/components/config_manager.py +106 -0
- wl_cli/commands/initenv/components/directory_utils.py +71 -0
- wl_cli/commands/initenv/components/git/__init__.py +9 -0
- wl_cli/commands/initenv/components/git/branch_creator.py +143 -0
- wl_cli/commands/initenv/components/git/git_flow_interface.py +24 -0
- wl_cli/commands/initenv/components/git/git_flow_manager.py +159 -0
- wl_cli/commands/initenv/components/git/git_lfs_manager.py +272 -0
- wl_cli/commands/initenv/components/git/git_utility.py +254 -0
- wl_cli/commands/initenv/components/git/gitignore_manager.py +53 -0
- wl_cli/commands/initenv/components/git_initializer.py +212 -0
- wl_cli/commands/initenv/components/i18n_manager.py +104 -0
- wl_cli/commands/initenv/components/log_manager.py +241 -0
- wl_cli/commands/initenv/components/project_structure/__init__.py +23 -0
- wl_cli/commands/initenv/components/project_structure/config_generator.py +360 -0
- wl_cli/commands/initenv/components/project_structure/directory_creator.py +184 -0
- wl_cli/commands/initenv/components/project_structure/hook_source.py +110 -0
- wl_cli/commands/initenv/components/project_structure/hooks_manager.py +995 -0
- wl_cli/commands/initenv/components/project_structure/readme_handler.py +56 -0
- wl_cli/commands/initenv/components/project_structure/rust_files_handler.py +30 -0
- wl_cli/commands/initenv/components/project_structure/setup_handler.py +103 -0
- wl_cli/commands/initenv/components/project_structure/sop_generator.py +73 -0
- wl_cli/commands/initenv/components/project_structure/template_copier.py +104 -0
- wl_cli/commands/initenv/components/project_structure/template_manager.py +195 -0
- wl_cli/commands/initenv/components/pyproject_generator.py +194 -0
- wl_cli/commands/initenv/components/rust_initializer.py +145 -0
- wl_cli/commands/initenv/components/symlink_cleaner.py +99 -0
- wl_cli/commands/initenv/components/symlink_manager.py +52 -0
- wl_cli/commands/initenv/components/venv_manager.py +118 -0
- wl_cli/commands/initenv/initenv.py +169 -0
- wl_cli/commands/initenv/services/__init__.py +11 -0
- wl_cli/commands/initenv/services/environment_initializer.py +288 -0
- wl_cli/commands/initenv/services/git_lfs_initializer.py +26 -0
- wl_cli/commands/initenv/services/project_structure_initializer.py +52 -0
- wl_cli/commands/lfs/README.md +221 -0
- wl_cli/commands/lfs/__init__.py +5 -0
- wl_cli/commands/lfs/cli.py +106 -0
- wl_cli/commands/lfs/config.py +164 -0
- wl_cli/commands/lfs/hook.py +61 -0
- wl_cli/commands/lfs/initializer.py +531 -0
- wl_cli/commands/lfs/manager.py +95 -0
- wl_cli/commands/lfs/policy.py +165 -0
- wl_cli/commands/lfs/watcher.py +294 -0
- wl_cli/commands/lint/__init__.py +1 -0
- wl_cli/commands/lint/lint_command.py +297 -0
- wl_cli/commands/lint/lint_executor.py +409 -0
- wl_cli/commands/lint/lint_reporter.py +579 -0
- wl_cli/commands/lint/lint_run.py +258 -0
- wl_cli/commands/lint/mypy_config.py +90 -0
- wl_cli/commands/registry.py +103 -0
- wl_cli/commands/self/__init__.py +7 -0
- wl_cli/commands/self/common/__init__.py +8 -0
- wl_cli/commands/self/common/capabilities.py +30 -0
- wl_cli/commands/self/common/config.py +27 -0
- wl_cli/commands/self/common/version_cache.py +85 -0
- wl_cli/commands/self/handlers/__init__.py +19 -0
- wl_cli/commands/self/handlers/base_handler.py +28 -0
- wl_cli/commands/self/handlers/install_handler.py +85 -0
- wl_cli/commands/self/handlers/status_handler.py +66 -0
- wl_cli/commands/self/handlers/uninstall_handler.py +58 -0
- wl_cli/commands/self/handlers/update_handler.py +102 -0
- wl_cli/commands/self/handlers/version_handler.py +16 -0
- wl_cli/commands/self/self_command.py +237 -0
- wl_cli/commands/self/services/__init__.py +15 -0
- wl_cli/commands/self/services/installation_manager.py +187 -0
- wl_cli/commands/self/services/local_installer.py +165 -0
- wl_cli/commands/self/services/pypi_updater.py +278 -0
- wl_cli/commands/self/services/windows_update.py +174 -0
- wl_cli/commands/testcommands/__init__.py +8 -0
- wl_cli/commands/testcommands/base_dependency_analyzer.py +198 -0
- wl_cli/commands/testcommands/batch_processor.py +278 -0
- wl_cli/commands/testcommands/cache_manager.py +1156 -0
- wl_cli/commands/testcommands/coverage_analyzer.py +955 -0
- wl_cli/commands/testcommands/coverage_cache_manager.py +183 -0
- wl_cli/commands/testcommands/progress_manager.py +79 -0
- wl_cli/commands/testcommands/python_env_detector.py +207 -0
- wl_cli/commands/testcommands/report_generator.py +101 -0
- wl_cli/commands/testcommands/single_test_executor.py +250 -0
- wl_cli/commands/testcommands/speed_checker.py +118 -0
- wl_cli/commands/testcommands/test_command_builder.py +165 -0
- wl_cli/commands/testcommands/test_commands.py +786 -0
- wl_cli/commands/testcommands/test_reporter.py +181 -0
- wl_cli/commands/testcommands/test_result_handler.py +96 -0
- wl_cli/commands/testcommands/test_runner.py +1343 -0
- wl_cli/commands/testcommands/test_utils.py +197 -0
- wl_cli/commands/testcommands/utils/__init__.py +17 -0
- wl_cli/commands/testcommands/utils/coverage_parser.py +98 -0
- wl_cli/commands/testcommands/utils/env_info.py +36 -0
- wl_cli/commands/testcommands/utils/junit_parser.py +75 -0
- wl_cli/commands/testcommands/utils/markdown_utils.py +20 -0
- wl_cli/commands/testcommands/utils/project_root.py +61 -0
- wl_cli/commands/update/__init__.py +9 -0
- wl_cli/commands/update/services/__init__.py +21 -0
- wl_cli/commands/update/services/backup_manager.py +207 -0
- wl_cli/commands/update/services/component_manager.py +83 -0
- wl_cli/commands/update/services/conflict_resolver.py +189 -0
- wl_cli/commands/update/services/vendors_updater.py +85 -0
- wl_cli/commands/update/services/version_tracker.py +127 -0
- wl_cli/commands/update/update_command.py +270 -0
- wl_cli/commands/update/utils/__init__.py +3 -0
- wl_cli/commands/version/__init__.py +9 -0
- wl_cli/commands/version/version_command.py +155 -0
- wl_cli/commands/workflow.py +477 -0
- wl_cli/env/__init__.py +5 -0
- wl_cli/env/cli.py +167 -0
- wl_cli/env_status.py +157 -0
- wl_cli/envelope.py +91 -0
- wl_cli/exceptions.py +85 -0
- wl_cli/lib/__init__.py +3 -0
- wl_cli/lib/rust_utils.py +35 -0
- wl_cli/mcp/__init__.py +6 -0
- wl_cli/mcp/capabilities.py +19 -0
- wl_cli/mcp/cli.py +22 -0
- wl_cli/mcp/gitlab.py +553 -0
- wl_cli/mcp/k8s.py +344 -0
- wl_cli/mcp/protocol.py +159 -0
- wl_cli/platform.py +139 -0
- wl_cli/quality/__init__.py +18 -0
- wl_cli/utils/__init__.py +7 -0
- wl_cli/utils/cache.py +491 -0
- wl_cli/utils/cache_ledger.py +355 -0
- wl_cli/utils/cache_store.py +546 -0
- wl_cli/utils/config/__init__.py +27 -0
- wl_cli/utils/config/config_loader.py +163 -0
- wl_cli/utils/config/core.py +862 -0
- wl_cli/utils/config/defaults.py +142 -0
- wl_cli/utils/config/env_exporter.py +86 -0
- wl_cli/utils/config/env_parser.py +108 -0
- wl_cli/utils/config/validation.py +280 -0
- wl_cli/utils/error_handler.py +165 -0
- wl_cli/utils/file_copier.py +56 -0
- wl_cli/utils/file_operations.py +506 -0
- wl_cli/utils/language_utils.py +122 -0
- wl_cli/utils/lazy_import.py +240 -0
- wl_cli/utils/log_levels.py +35 -0
- wl_cli/utils/log_rotators.py +74 -0
- wl_cli/utils/logging.py +458 -0
- wl_cli/utils/output_context.py +142 -0
- wl_cli/utils/performance/__init__.py +1 -0
- wl_cli/utils/performance/decorators.py +111 -0
- wl_cli/utils/performance/exporter.py +38 -0
- wl_cli/utils/performance/models.py +16 -0
- wl_cli/utils/performance/monitor.py +161 -0
- wl_cli/utils/performance_monitor.py +32 -0
- wl_cli/utils/platform_adapter.py +131 -0
- wl_cli/utils/pre_commit.py +266 -0
- wl_cli/utils/pre_commit_config.py +72 -0
- wl_cli/utils/root.py +521 -0
- wl_cli/utils/root_fallback.py +256 -0
- wl_cli/utils/run_evidence.py +236 -0
- wl_cli/utils/structured_logger.py +18 -0
- wl_cli/utils/structured_logging/__init__.py +19 -0
- wl_cli/utils/structured_logging/config.py +337 -0
- wl_cli/utils/structured_logging/core.py +177 -0
- wl_cli/utils/structured_logging/env.py +56 -0
- wl_cli/utils/structured_logging/filtering.py +75 -0
- wl_cli/utils/structured_logging/filters.py +55 -0
- wl_cli/utils/structured_logging/formatters.py +251 -0
- wl_cli/utils/structured_logging/handlers.py +308 -0
- wl_cli/utils/structured_logging/logger.py +354 -0
- wl_cli/utils/subprocess.py +71 -0
- wl_cli/utils/subprocess_utils.py +260 -0
- wl_cli/utils/test_utils/__init__.py +5 -0
- wl_cli/utils/test_utils/test_filter.py +36 -0
- wl_cli/utils/uv_run.py +79 -0
- wl_cli/utils/uv_tool.py +117 -0
- wl_cli/utils/uv_tree.py +71 -0
- wl_cli/utils/uv_workspace.py +174 -0
- wl_cli/utils/version/__init__.py +15 -0
- wl_cli/utils/version/pypi_version_checker.py +95 -0
- wl_cli/utils/version/version_comparator.py +133 -0
- wl_cli/utils/version/version_detectors.py +312 -0
- wl_cli/utils/version/version_service.py +131 -0
- wl_cli/utils/version/version_updater.py +198 -0
- wl_cli/utils/wl_dir_updater.py +1440 -0
- wl_cli/utils/workspace_detector/__init__.py +28 -0
- wl_cli/utils/workspace_detector/core.py +161 -0
- wl_cli/utils/workspace_detector/detection_strategies.py +107 -0
- wl_cli/utils/workspace_detector/exceptions.py +5 -0
- wl_cli/utils/workspace_detector/types.py +17 -0
- wl_cli/utils/workspace_detector/venv_resolver.py +84 -0
- wl_cli/vendors/config/.codespellrc +2 -0
- wl_cli/vendors/config/dependencies.toml +10 -0
- wl_cli/vendors/config/mypy.ini +62 -0
- wl_cli/vendors/config/pyproject_defaults.toml +28 -0
- wl_cli/vendors/config/template_config.toml +24 -0
- wl_cli/vendors/config/tool_configs.toml +76 -0
- wl_cli/vendors/git/.gitignore +122 -0
- wl_cli/vendors/git/.pre-commit-config.yaml +37 -0
- wl_cli/vendors/hooks/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/post-update.sample +8 -0
- wl_cli/vendors/hooks/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/pre-commit +45 -0
- wl_cli/vendors/hooks/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/pre-push.sample +60 -0
- wl_cli/vendors/hooks/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/push-to-checkout.sample +78 -0
- wl_cli/vendors/hooks/sendemail-validate.sample +77 -0
- wl_cli/vendors/hooks/unix/applypatch-msg.sample +15 -0
- wl_cli/vendors/hooks/unix/check_lockfile.py +58 -0
- wl_cli/vendors/hooks/unix/check_lockfile.sh +36 -0
- wl_cli/vendors/hooks/unix/commit-msg.sample +24 -0
- wl_cli/vendors/hooks/unix/fsmonitor-watchman.sample +174 -0
- wl_cli/vendors/hooks/unix/post-update.sample +8 -0
- wl_cli/vendors/hooks/unix/pre-applypatch.sample +14 -0
- wl_cli/vendors/hooks/unix/pre-commit +190 -0
- wl_cli/vendors/hooks/unix/pre-commit.legacy +31 -0
- wl_cli/vendors/hooks/unix/pre-commit.py +81 -0
- wl_cli/vendors/hooks/unix/pre-commit.sample +49 -0
- wl_cli/vendors/hooks/unix/pre-merge-commit.sample +13 -0
- wl_cli/vendors/hooks/unix/pre-push +417 -0
- wl_cli/vendors/hooks/unix/pre-push.sample +53 -0
- wl_cli/vendors/hooks/unix/pre-rebase.sample +169 -0
- wl_cli/vendors/hooks/unix/pre-receive.sample +24 -0
- wl_cli/vendors/hooks/unix/prepare-commit-msg.sample +42 -0
- wl_cli/vendors/hooks/update.sample +128 -0
- wl_cli/vendors/hooks/win/work.bat +63 -0
- wl_cli/vendors/project/.gitignore +90 -0
- wl_cli/vendors/project/.python-version +1 -0
- wl_cli/vendors/project/LICENSE +21 -0
- wl_cli/vendors/project/README.md +0 -0
- wl_cli/vendors/project/pyproject.toml +27 -0
- wl_cli/vendors/project/pyproject_no_workspace.toml +16 -0
- wl_cli/vendors/project/pyproject_workspace.toml +14 -0
- wl_cli/vendors/readme/README.md +120 -0
- wl_cli/vendors/rust/Cargo.toml +14 -0
- wl_cli/vendors/rust/lib/__init__.py +3 -0
- wl_cli/vendors/rust/lib/rust_utils.py +31 -0
- wl_cli/vendors/rust/src/lib.rs +13 -0
- wl_cli/vendors/rust/src/main.rs +3 -0
- wl_cli/vendors/rust/tests/test_rust_fallback.py +40 -0
- wl_cli/vendors/sop_doc/git-dev-sop.md +220 -0
- wl_cli/vendors/sop_doc/industrial-standard-sop.md +145 -0
- wl_cli/vendors/sop_doc/python-sop.md +342 -0
- wl_cli/vendors/sop_doc/rust-readme.md +219 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""WL configuration generator."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
|
|
6
|
+
import tomllib
|
|
7
|
+
|
|
8
|
+
from .....utils.file_copier import FileCopier
|
|
9
|
+
from .....utils.logging import log_debug_pair, log_info, log_info_pair
|
|
10
|
+
from .....utils.root import Root, RootKind
|
|
11
|
+
from .....utils.root_fallback import current_root
|
|
12
|
+
from ..config_manager import ConfigManager
|
|
13
|
+
|
|
14
|
+
#: A bare relative :class:`Root`: the ``.wl`` layout has one definition, in the
|
|
15
|
+
#: root module, so its *relative* spelling is derived rather than re-typed here
|
|
16
|
+
#: (#118). The kind is irrelevant -- the derived-path methods only join fixed
|
|
17
|
+
#: names onto ``path`` -- so PROJECT is the neutral placeholder, not a claim.
|
|
18
|
+
_LAYOUT = Root(Path("."), RootKind.PROJECT)
|
|
19
|
+
|
|
20
|
+
#: The manifest filename under ``.wl``: the one spelling shared by the absolute
|
|
21
|
+
#: :func:`template_manifest_path` and the relative
|
|
22
|
+
#: :attr:`ConfigGenerator.TEMPLATE_MANIFEST`, so the two cannot name different
|
|
23
|
+
#: files.
|
|
24
|
+
_MANIFEST_NAME = ".templates.json"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def template_manifest_path(deploy_root: Root | None = None) -> Path:
|
|
28
|
+
"""Path of the config-template deployment manifest under the deploy root.
|
|
29
|
+
|
|
30
|
+
``<hooks deploy root>/.wl/.templates.json`` -- anchored exactly like the
|
|
31
|
+
templates it tracks, so the manifest and the files it describes always share
|
|
32
|
+
one ``.wl``.
|
|
33
|
+
|
|
34
|
+
``deploy_root`` may be supplied as a value by the self-sync chain, which no
|
|
35
|
+
longer chdirs to it; when omitted this single-root entry resolves exactly
|
|
36
|
+
the one root it needs through
|
|
37
|
+
:func:`~wl_cli.utils.root_fallback.current_root` (#160).
|
|
38
|
+
"""
|
|
39
|
+
root = deploy_root if deploy_root is not None else current_root(RootKind.DEPLOY)
|
|
40
|
+
return root.wl_dir() / _MANIFEST_NAME
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ConfigGenerator:
|
|
44
|
+
"""Generate configuration files in .wl directory using config-driven approach."""
|
|
45
|
+
|
|
46
|
+
#: Manifest tracking which config template source content was last deployed,
|
|
47
|
+
#: relative to the ``.wl`` at the hooks deploy root (see
|
|
48
|
+
#: :func:`template_manifest_path`). Kept a relative class constant so that
|
|
49
|
+
#: constructing a generator never shells out to git; it is anchored at use
|
|
50
|
+
#: time by :meth:`_manifest_path`. Tests override it on the instance.
|
|
51
|
+
#:
|
|
52
|
+
#: The ``.wl`` half is derived from the root module (#118) rather than
|
|
53
|
+
#: re-spelled here, so the control-directory name has a single definition;
|
|
54
|
+
#: the filename half is shared with :func:`template_manifest_path` through
|
|
55
|
+
#: :data:`_MANIFEST_NAME`, so the two can never name different files.
|
|
56
|
+
TEMPLATE_MANIFEST = _LAYOUT.wl_dir() / _MANIFEST_NAME
|
|
57
|
+
|
|
58
|
+
def __init__(self):
|
|
59
|
+
self.config_manager = ConfigManager()
|
|
60
|
+
self.file_copier = FileCopier()
|
|
61
|
+
self.config = {}
|
|
62
|
+
self._load_config()
|
|
63
|
+
|
|
64
|
+
def _load_config(self):
|
|
65
|
+
config_path = self.config_manager.get_vendor_config_path(
|
|
66
|
+
"config/template_config.toml"
|
|
67
|
+
)
|
|
68
|
+
if config_path.exists():
|
|
69
|
+
try:
|
|
70
|
+
with open(config_path, "rb") as f:
|
|
71
|
+
self.config = tomllib.load(f)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
log_info(f"Warning: Failed to load template config: {e}")
|
|
74
|
+
self.config = {}
|
|
75
|
+
else:
|
|
76
|
+
self.config = {}
|
|
77
|
+
|
|
78
|
+
def generate_configs(self, deploy_root: Root | None = None) -> bool:
|
|
79
|
+
"""Generate configuration files in .wl directory.
|
|
80
|
+
|
|
81
|
+
Pure template migration: this never re-syncs git hooks. Callers that
|
|
82
|
+
need hooks synced invoke :func:`hooks_manager.sync_hooks` explicitly at
|
|
83
|
+
their orchestration site.
|
|
84
|
+
|
|
85
|
+
``deploy_root`` is the root the ``.wl`` files are written under, threaded
|
|
86
|
+
in as a value by the self-sync chain (which no longer chdirs to it); when
|
|
87
|
+
omitted this single-root entry resolves exactly the one root it needs
|
|
88
|
+
through :func:`~wl_cli.utils.root_fallback.current_root` (#160).
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True when ``.wl/config.json`` exists once this returns *and* the
|
|
92
|
+
template manifest was saved. The verdict is read off the disk, not
|
|
93
|
+
off the creation attempt: under pytest the incidental-save guard
|
|
94
|
+
used to answer "written" for a file it never created, so the sync
|
|
95
|
+
deployed templates onto a ``.wl`` with no config and then failed the
|
|
96
|
+
version stamp (#202). The manifest save reports its own outcome
|
|
97
|
+
(#201), so an unwritable manifest cannot leave the sync claiming a
|
|
98
|
+
refresh it did not persist. Callers take False as a failed config
|
|
99
|
+
sub-step instead of a silently half-built ``.wl``.
|
|
100
|
+
"""
|
|
101
|
+
if deploy_root is None:
|
|
102
|
+
deploy_root = current_root(RootKind.DEPLOY)
|
|
103
|
+
wl_dir = deploy_root.wl_dir()
|
|
104
|
+
log_dir = wl_dir / "log"
|
|
105
|
+
config_file = wl_dir / "config.json"
|
|
106
|
+
|
|
107
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
log_debug_pair("✓ Created .wl/log directory", "✓ 创建 .wl/log 目录")
|
|
109
|
+
|
|
110
|
+
if not config_file.exists():
|
|
111
|
+
try:
|
|
112
|
+
from .....utils.config import ConfigManager as ProjectConfigManager
|
|
113
|
+
from .....utils.config import get_config_manager
|
|
114
|
+
|
|
115
|
+
config_manager = get_config_manager()
|
|
116
|
+
if _same_file(Path(config_manager.config_file), config_file):
|
|
117
|
+
materialised = config_manager.ensure_project_config()
|
|
118
|
+
else:
|
|
119
|
+
# The process-wide manager writes wherever its own resolution
|
|
120
|
+
# points -- ``WL_CONFIG_PATH``, or a CWD outside the checkout.
|
|
121
|
+
# Asking it to "ensure the project config" then materialised
|
|
122
|
+
# the defaults in that unrelated file while this ``.wl`` kept
|
|
123
|
+
# no ``config.json`` at all: the self-sync read "missing
|
|
124
|
+
# config" on every command and never converged (#190). Write
|
|
125
|
+
# the defaults at the ``.wl`` this sync actually owns.
|
|
126
|
+
log_info_pair(
|
|
127
|
+
f"Creating the default config at {config_file} (the active "
|
|
128
|
+
f"config file is {config_manager.config_file}).",
|
|
129
|
+
f"正在 {config_file} 创建默认配置(当前生效的配置文件是 "
|
|
130
|
+
f"{config_manager.config_file})。",
|
|
131
|
+
)
|
|
132
|
+
materialised = ProjectConfigManager(
|
|
133
|
+
config_file=str(config_file)
|
|
134
|
+
).ensure_project_config()
|
|
135
|
+
# ``ensure_project_config`` already answers by existence; the
|
|
136
|
+
# re-check keeps the contract honest even for a mocked or
|
|
137
|
+
# subclassed manager that only claims success (#202).
|
|
138
|
+
if not materialised or not config_file.exists():
|
|
139
|
+
raise RuntimeError(f"no config.json was created at {config_file}")
|
|
140
|
+
log_debug_pair(
|
|
141
|
+
f"✓ Created default config file at {config_file}",
|
|
142
|
+
f"✓ 在 {config_file} 创建默认配置文件",
|
|
143
|
+
)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
log_info_pair(
|
|
146
|
+
f"Warning: Failed to create default config file: {e}",
|
|
147
|
+
f"警告: 创建默认配置文件失败: {e}",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
templates_saved = self._copy_config_templates(deploy_root.path)
|
|
151
|
+
|
|
152
|
+
# Report from the bytes on disk, never from "a creation was attempted":
|
|
153
|
+
# a half-built ``.wl`` (templates yes, config no) is what let the first
|
|
154
|
+
# worktree run fail its version stamp under the test-environment save
|
|
155
|
+
# guard (#202). The manifest's own save is part of the verdict too: a
|
|
156
|
+
# stale manifest forces the next command to rebuild, so claiming success
|
|
157
|
+
# for a save that failed is the silent-retry loop of #201.
|
|
158
|
+
return config_file.exists() and templates_saved
|
|
159
|
+
|
|
160
|
+
def _copy_config_templates(self, deploy_root: Path | None = None) -> bool:
|
|
161
|
+
"""Copy config templates, refreshing tool-deployed ones when the source
|
|
162
|
+
template changed, while preserving files the user has modified.
|
|
163
|
+
|
|
164
|
+
Deployment is tracked in ``.wl/.templates.json``. Each entry is either a
|
|
165
|
+
legacy scalar (source sha256 last deployed, treated as tool-owned) or a
|
|
166
|
+
v2 record ``{"src": <sha>, "user": bool}``:
|
|
167
|
+
|
|
168
|
+
- ``user: true`` -> the destination is (or became) user-modified: it is
|
|
169
|
+
never refreshed (bug #44). The tool stops auto-propagating to it.
|
|
170
|
+
- ``user: false`` -> tool-owned: refreshed when the vendored source
|
|
171
|
+
changes AND the destination is still byte-identical to the last
|
|
172
|
+
deployed source; once the destination diverges (the user touched it)
|
|
173
|
+
it is re-pinned as ``user: true`` and preserved.
|
|
174
|
+
|
|
175
|
+
A pre-existing ``.wl`` without a manifest entry is migrated once:
|
|
176
|
+
destinations already matching the source are recorded as tool-owned;
|
|
177
|
+
destinations that differ may be user-modified, so they are recorded as
|
|
178
|
+
``user: true`` and never clobbered.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Whether the manifest was persisted; the template copies themselves
|
|
182
|
+
stay best-effort (their failures are already reported per file).
|
|
183
|
+
"""
|
|
184
|
+
# Direct callers (tests) may omit the path; the single root this helper
|
|
185
|
+
# needs then comes from the one fallback policy. The real chain always
|
|
186
|
+
# passes ``generate_configs``' resolved root down (#160).
|
|
187
|
+
if deploy_root is None:
|
|
188
|
+
deploy_root = current_root(RootKind.DEPLOY).path
|
|
189
|
+
|
|
190
|
+
config_templates = self.config.get("config_templates", {})
|
|
191
|
+
manifest = self._load_template_manifest(deploy_root)
|
|
192
|
+
|
|
193
|
+
for name, template in config_templates.items():
|
|
194
|
+
src = self.config_manager.get_vendor_config_path(template["src"])
|
|
195
|
+
dest = deploy_root / template["dest"]
|
|
196
|
+
|
|
197
|
+
if not src.exists():
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
src_hash = _sha256(src)
|
|
201
|
+
if not dest.exists():
|
|
202
|
+
if self._copy_template(name, src, dest):
|
|
203
|
+
manifest[name] = {"src": src_hash, "user": False}
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
if name not in manifest:
|
|
207
|
+
# Pre-manifest deployment: record intent; never clobber here.
|
|
208
|
+
if _sha256(dest) == src_hash:
|
|
209
|
+
manifest[name] = {"src": src_hash, "user": False}
|
|
210
|
+
else:
|
|
211
|
+
# Existing dest differing from source may be user-modified
|
|
212
|
+
# (bug #44): preserve it and pin it as user-owned so the
|
|
213
|
+
# tool never auto-propagates over it.
|
|
214
|
+
manifest[name] = {"src": src_hash, "user": True}
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
entry = self._normalize_entry(manifest[name])
|
|
218
|
+
if entry["user"]:
|
|
219
|
+
# User-owned: never refresh, never clobber (bug #44).
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
dest_hash = _sha256(dest)
|
|
223
|
+
if entry["src"] == src_hash:
|
|
224
|
+
# Source unchanged since last deploy; detect a post-deploy edit.
|
|
225
|
+
if dest_hash != entry["src"]:
|
|
226
|
+
manifest[name] = {"src": src_hash, "user": True}
|
|
227
|
+
else:
|
|
228
|
+
# Vendored source changed: refresh only a pristine tool copy.
|
|
229
|
+
if dest_hash == entry["src"]:
|
|
230
|
+
if self._copy_template(name, src, dest):
|
|
231
|
+
manifest[name] = {"src": src_hash, "user": False}
|
|
232
|
+
else:
|
|
233
|
+
# Destination diverged from the last deployed source.
|
|
234
|
+
manifest[name] = {"src": src_hash, "user": True}
|
|
235
|
+
|
|
236
|
+
return self._save_template_manifest(manifest, deploy_root)
|
|
237
|
+
|
|
238
|
+
def missing_template_destinations(self, deploy_root: Root) -> list[Path]:
|
|
239
|
+
"""Destinations the manifest tracks as tool-owned that are now missing (#190).
|
|
240
|
+
|
|
241
|
+
The manifest's *presence* used to be the whole freshness answer, so a
|
|
242
|
+
template deleted from ``.wl`` was never copied back: ``wl format`` then
|
|
243
|
+
silently skipped its hooks and ``wl lint`` failed with no way to heal
|
|
244
|
+
itself. A per-entry ``exists()`` is what the manifest can actually be
|
|
245
|
+
held to here.
|
|
246
|
+
|
|
247
|
+
Deliberately **not** a content/hash check: an existing destination stays
|
|
248
|
+
untouched, so the "a manifest means do not overwrite user edits" policy
|
|
249
|
+
is preserved. Entries pinned as ``user: true`` are skipped -- the tool
|
|
250
|
+
never re-propagates to them, and recreating them would make the check
|
|
251
|
+
permanently true (a fresh never-converging loop).
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
deploy_root: The root the destinations were deployed under.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
The missing destination paths, in manifest order.
|
|
258
|
+
"""
|
|
259
|
+
deploy_path = deploy_root.path
|
|
260
|
+
manifest = self._load_template_manifest(deploy_path)
|
|
261
|
+
templates = self.config.get("config_templates", {})
|
|
262
|
+
|
|
263
|
+
missing: list[Path] = []
|
|
264
|
+
for name, value in manifest.items():
|
|
265
|
+
if self._normalize_entry(value)["user"]:
|
|
266
|
+
continue
|
|
267
|
+
template = templates.get(name)
|
|
268
|
+
if template is None:
|
|
269
|
+
# The template is no longer shipped; a missing dest is expected.
|
|
270
|
+
continue
|
|
271
|
+
dest = deploy_path / template["dest"]
|
|
272
|
+
if not dest.exists():
|
|
273
|
+
missing.append(dest)
|
|
274
|
+
return missing
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def _normalize_entry(value):
|
|
278
|
+
"""Normalize a manifest entry (legacy scalar or v2 record) to v2."""
|
|
279
|
+
if isinstance(value, dict):
|
|
280
|
+
return {"src": value.get("src", ""), "user": bool(value.get("user", False))}
|
|
281
|
+
if isinstance(value, str):
|
|
282
|
+
return {"src": value, "user": False}
|
|
283
|
+
return {"src": "", "user": False}
|
|
284
|
+
|
|
285
|
+
def _copy_template(self, name: str, src: Path, dest: Path) -> bool:
|
|
286
|
+
# Refreshing a template intentionally overwrites the destination; the
|
|
287
|
+
# caller has already decided (see _copy_config_templates) that the
|
|
288
|
+
# destination is a pristine tool copy, not user-modified.
|
|
289
|
+
result = self.file_copier.copy_file(src, dest, overwrite=True)
|
|
290
|
+
if result.success:
|
|
291
|
+
log_debug_pair(f"✓ Copied {name} from template", f"✓ 从模板复制 {name}")
|
|
292
|
+
return True
|
|
293
|
+
log_info(f"Warning: Failed to copy {name}: {result.message}")
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
def _manifest_path(self, deploy_root: Path) -> Path:
|
|
297
|
+
"""The manifest path resolved under ``deploy_root`` when it is relative.
|
|
298
|
+
|
|
299
|
+
``TEMPLATE_MANIFEST`` stays a relative class constant so constructors
|
|
300
|
+
never run git; an absolute override -- as the tests supply -- is used
|
|
301
|
+
unchanged.
|
|
302
|
+
"""
|
|
303
|
+
manifest = self.TEMPLATE_MANIFEST
|
|
304
|
+
return manifest if manifest.is_absolute() else deploy_root / manifest
|
|
305
|
+
|
|
306
|
+
def _load_template_manifest(self, deploy_root: Path) -> dict[str, Any]:
|
|
307
|
+
import json
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
return cast(
|
|
311
|
+
"dict[str, Any]",
|
|
312
|
+
json.loads(
|
|
313
|
+
self._manifest_path(deploy_root).read_text(encoding="utf-8")
|
|
314
|
+
),
|
|
315
|
+
)
|
|
316
|
+
except (OSError, json.JSONDecodeError):
|
|
317
|
+
return {}
|
|
318
|
+
|
|
319
|
+
def _save_template_manifest(self, manifest: dict, deploy_root: Path) -> bool:
|
|
320
|
+
"""Persist the manifest; report (not swallow) a save that failed.
|
|
321
|
+
|
|
322
|
+
The save is the only place that knows whether the manifest actually
|
|
323
|
+
landed, so its verdict is returned instead of logged and dropped (#201):
|
|
324
|
+
a caller that answers "refreshed" from ``config.json`` alone reports
|
|
325
|
+
success for a manifest the process could not write, and every later
|
|
326
|
+
command re-runs the rebuild. Returning False lets :meth:`generate_configs`
|
|
327
|
+
fail the sub-step and engage the cooldown -- including the
|
|
328
|
+
*missing-manifest-in-a-read-only-``.wl``* state, where no path probe can
|
|
329
|
+
answer for the write that will actually be attempted.
|
|
330
|
+
"""
|
|
331
|
+
import json
|
|
332
|
+
|
|
333
|
+
path = self._manifest_path(deploy_root)
|
|
334
|
+
try:
|
|
335
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
336
|
+
path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
|
337
|
+
except OSError as e:
|
|
338
|
+
log_info_pair(
|
|
339
|
+
f"Warning: Failed to save template manifest: {e}",
|
|
340
|
+
f"警告: 保存模板 manifest 失败: {e}",
|
|
341
|
+
)
|
|
342
|
+
return False
|
|
343
|
+
return True
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _same_file(a: Path, b: Path) -> bool:
|
|
347
|
+
"""Whether two paths name the same file (tolerating symlinks)."""
|
|
348
|
+
try:
|
|
349
|
+
return a.resolve() == b.resolve()
|
|
350
|
+
except OSError:
|
|
351
|
+
return a == b
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _sha256(path: Path) -> str:
|
|
355
|
+
import hashlib
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
359
|
+
except OSError:
|
|
360
|
+
return ""
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Project directory structure creator."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .....utils.logging import log_debug_pair, log_info_pair
|
|
7
|
+
from ..config_manager import ConfigManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def normalize_project_name(project_name: str) -> str:
|
|
11
|
+
"""Normalize a project name into a valid Python package identifier.
|
|
12
|
+
|
|
13
|
+
This is the single public implementation (#175) shared by directory
|
|
14
|
+
creation, template rendering and pyproject generation. It merges the
|
|
15
|
+
former ``_normalize_project_name`` and ``clean_python_package_name``
|
|
16
|
+
semantics: hyphens, dots, spaces and any other invalid characters
|
|
17
|
+
collapse to one underscore, leading/trailing underscores are stripped,
|
|
18
|
+
a leading digit is prefixed with ``_``, and an empty result falls back
|
|
19
|
+
to ``python_package``.
|
|
20
|
+
"""
|
|
21
|
+
cleaned = re.sub(r"[^a-zA-Z0-9_]+", "_", project_name).strip("_")
|
|
22
|
+
if not cleaned:
|
|
23
|
+
return "python_package"
|
|
24
|
+
if cleaned[0].isdigit():
|
|
25
|
+
return f"_{cleaned}"
|
|
26
|
+
return cleaned
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Backwards-compatible private spelling: one implementation behind both names.
|
|
30
|
+
_normalize_project_name = normalize_project_name
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _create_required_directories(project_name: str) -> None:
|
|
34
|
+
"""Create required project directories."""
|
|
35
|
+
normalized_name = _normalize_project_name(project_name)
|
|
36
|
+
directories = [
|
|
37
|
+
"src",
|
|
38
|
+
f"src/{normalized_name}",
|
|
39
|
+
f"src/{normalized_name}/lib", # 添加lib目录以支持Rust扩展
|
|
40
|
+
"tests",
|
|
41
|
+
"docs",
|
|
42
|
+
"examples",
|
|
43
|
+
"rust",
|
|
44
|
+
"dist",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
for directory in directories:
|
|
48
|
+
Path(directory).mkdir(parents=True, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _create_init_files(project_name: str) -> None:
|
|
52
|
+
"""Create __init__.py files for Python packages."""
|
|
53
|
+
normalized_name = normalize_project_name(project_name)
|
|
54
|
+
directories = [
|
|
55
|
+
"src",
|
|
56
|
+
f"src/{normalized_name}",
|
|
57
|
+
f"src/{normalized_name}/lib",
|
|
58
|
+
"tests",
|
|
59
|
+
"docs",
|
|
60
|
+
"examples",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
for directory in directories:
|
|
64
|
+
if Path(directory).exists():
|
|
65
|
+
init_file = Path(directory) / "__init__.py"
|
|
66
|
+
if not init_file.exists():
|
|
67
|
+
init_file.touch()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _create_readme(project_name: str) -> None:
|
|
71
|
+
"""Create README.md file if it doesn't exist or is empty."""
|
|
72
|
+
readme_file = Path("README.md")
|
|
73
|
+
|
|
74
|
+
# Only create README if it doesn't exist or is empty
|
|
75
|
+
should_create = (
|
|
76
|
+
not readme_file.exists()
|
|
77
|
+
or readme_file.read_text(encoding="utf-8").strip() == ""
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if should_create:
|
|
81
|
+
# Try to get project description from config
|
|
82
|
+
try:
|
|
83
|
+
config_manager = ConfigManager()
|
|
84
|
+
description = config_manager.get(
|
|
85
|
+
"project_description", f"A Python project: {project_name}"
|
|
86
|
+
)
|
|
87
|
+
except Exception:
|
|
88
|
+
# Fallback to default description if config fails
|
|
89
|
+
description = f"A Python project: {project_name}"
|
|
90
|
+
|
|
91
|
+
readme_content = f"""# {project_name}
|
|
92
|
+
|
|
93
|
+
{description}
|
|
94
|
+
|
|
95
|
+
## Installation
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pip install {project_name}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Usage
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import {project_name}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Contributing
|
|
108
|
+
|
|
109
|
+
1. Fork the repository
|
|
110
|
+
2. Create a feature branch
|
|
111
|
+
3. Make your changes
|
|
112
|
+
4. Add tests
|
|
113
|
+
5. Submit a pull request
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
readme_file.write_text(readme_content, encoding="utf-8")
|
|
121
|
+
log_debug_pair("✓ README.md created successfully", "✓ README.md 创建成功")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def setup_project_structure(project_name: str) -> None:
|
|
125
|
+
"""Setup complete project structure by calling all required functions."""
|
|
126
|
+
_create_required_directories(project_name)
|
|
127
|
+
_create_init_files(project_name)
|
|
128
|
+
_create_readme(project_name)
|
|
129
|
+
|
|
130
|
+
# Copy and configure git hooks
|
|
131
|
+
from .hooks_manager import sync_hooks
|
|
132
|
+
|
|
133
|
+
sync_hooks()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class DirectoryCreator:
|
|
137
|
+
"""Create project directory structure."""
|
|
138
|
+
|
|
139
|
+
def create_structure(self, project_name: str, workspace_mode: bool = False) -> None:
|
|
140
|
+
"""Create main project directory structure.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
project_name: Name of the project
|
|
144
|
+
workspace_mode: If True, create workspace layout (packages/{name}/)
|
|
145
|
+
If False, create flat layout (src/{name}/)
|
|
146
|
+
"""
|
|
147
|
+
if workspace_mode:
|
|
148
|
+
_create_workspace_directories(project_name)
|
|
149
|
+
else:
|
|
150
|
+
_create_required_directories(project_name)
|
|
151
|
+
_create_init_files(project_name)
|
|
152
|
+
|
|
153
|
+
log_info_pair("✓ Project structure created successfully", "✓ 项目结构创建成功")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _create_workspace_directories(project_name: str) -> None:
|
|
157
|
+
"""Create workspace-style project directories under packages/{name}/."""
|
|
158
|
+
normalized_name = normalize_project_name(project_name)
|
|
159
|
+
package_dir = f"packages/{normalized_name}"
|
|
160
|
+
directories = [
|
|
161
|
+
package_dir,
|
|
162
|
+
f"{package_dir}/src",
|
|
163
|
+
f"{package_dir}/src/{normalized_name}",
|
|
164
|
+
f"{package_dir}/src/{normalized_name}/lib",
|
|
165
|
+
f"{package_dir}/rust",
|
|
166
|
+
f"{package_dir}/rust/src",
|
|
167
|
+
f"{package_dir}/tests",
|
|
168
|
+
"docs",
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
for directory in directories:
|
|
172
|
+
Path(directory).mkdir(parents=True, exist_ok=True)
|
|
173
|
+
|
|
174
|
+
# Create __init__.py files for package
|
|
175
|
+
init_dirs = [
|
|
176
|
+
f"{package_dir}/src",
|
|
177
|
+
f"{package_dir}/src/{normalized_name}",
|
|
178
|
+
f"{package_dir}/src/{normalized_name}/lib",
|
|
179
|
+
f"{package_dir}/tests",
|
|
180
|
+
]
|
|
181
|
+
for directory in init_dirs:
|
|
182
|
+
init_file = Path(directory) / "__init__.py"
|
|
183
|
+
if not init_file.exists():
|
|
184
|
+
init_file.touch()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""The one place that says where the git-hook templates live (#162).
|
|
2
|
+
|
|
3
|
+
Three spellings of "the vendored hooks template directory" used to coexist --
|
|
4
|
+
the deployment call, the staleness gate's private accessor, and a CWD-relative
|
|
5
|
+
literal in ``clean lfs`` -- so a layout change could be made in one place and
|
|
6
|
+
silently missed in another. :func:`locate_hook_source` is now the only
|
|
7
|
+
definition; every consumer takes its directories from the value it returns.
|
|
8
|
+
|
|
9
|
+
**Platform policy, made explicit.** All platforms deploy the ``unix`` templates
|
|
10
|
+
(Git Bash executes them on Windows). That used to be expressed only by passing
|
|
11
|
+
the already platform-qualified key ``hooks/unix`` to
|
|
12
|
+
:meth:`ConfigManager.get_vendor_config_path`, whose platform substitution fires
|
|
13
|
+
only for keys *without* a ``/win`` or ``/unix`` segment -- i.e. the decision was
|
|
14
|
+
encoded in the shape of a string, and the one place that *did* pass the bare
|
|
15
|
+
``hooks`` key silently resolved to ``hooks/win`` on Windows. Here the policy is
|
|
16
|
+
the point of the module: the key is ``hooks/unix`` on every platform, and a
|
|
17
|
+
missing unix directory is reported as :attr:`HookSource.template_missing` -- it
|
|
18
|
+
never falls back to the bare key, which would put a different template set
|
|
19
|
+
behind the same locator.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from .....utils.root import Root
|
|
26
|
+
from ..config_manager import ConfigManager
|
|
27
|
+
|
|
28
|
+
#: The vendored template subtree every platform deploys from. Explicitly
|
|
29
|
+
#: unix-qualified so ``get_vendor_config_path``'s platform substitution -- which
|
|
30
|
+
#: only fires for keys without a ``/win`` or ``/unix`` segment -- never rewrites
|
|
31
|
+
#: it. See the module docstring for why that is the policy, not an accident.
|
|
32
|
+
_HOOK_TEMPLATE_KEY = "hooks/unix"
|
|
33
|
+
|
|
34
|
+
#: There is no shared-config literal any more (ADR-0009 supersedes ADR-0005):
|
|
35
|
+
#: ``wl`` writes a **worktree-scoped** ``core.hooksPath``, and that value is
|
|
36
|
+
#: derived from the deploy root by :func:`locate_hook_source` -- see
|
|
37
|
+
#: :attr:`HookSource.worktree_hooks_path`. Keeping one derived value for both
|
|
38
|
+
#: the write side and the comparison side is what stops them drifting apart.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class HookSource:
|
|
43
|
+
"""Where hooks come from and where they go, in one value (#162).
|
|
44
|
+
|
|
45
|
+
Frozen because it is a snapshot: fields are read once at construction and
|
|
46
|
+
every later read returns that same answer, so one consumer cannot observe
|
|
47
|
+
the filesystem changing underneath it. That is why
|
|
48
|
+
:attr:`template_missing` is an ordinary field -- probed **once**, here at
|
|
49
|
+
construction -- rather than a property that re-stats the filesystem on
|
|
50
|
+
every access. ``apply_hook_sync`` branches on the flag and then warns on it;
|
|
51
|
+
a property would let a template directory that appeared or vanished in
|
|
52
|
+
between flip one read and not the other, producing either a silently
|
|
53
|
+
skipped copy/prune pass (branch saw "missing", warning saw "present") or a
|
|
54
|
+
prune pass off a template it then reports as missing (the reverse).
|
|
55
|
+
``evaluate_hooks`` and ``apply_hook_sync`` call :func:`locate_hook_source`
|
|
56
|
+
independently, so each gets its own snapshot; the guarantee here is
|
|
57
|
+
consistency *within* one call, not sameness across the two.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
#: Vendored template directory (``hooks/unix``) -- the same on every
|
|
61
|
+
#: platform, because Git Bash runs the unix hooks on Windows too.
|
|
62
|
+
template_dir: Path
|
|
63
|
+
#: The deploy root's ``.wl/hooks`` -- the directory ``core.hooksPath``
|
|
64
|
+
#: resolves to in the checkout being deployed into.
|
|
65
|
+
deploy_dir: Path
|
|
66
|
+
#: The **absolute** path written to the worktree-scoped ``core.hooksPath``
|
|
67
|
+
#: and compared against it (ADR-0009). Derived from the same deploy root as
|
|
68
|
+
#: :attr:`deploy_dir`, so "where the hooks were deployed" and "where the
|
|
69
|
+
#: pointer says they are" cannot disagree. Consumed by ``hooks_manager`` on
|
|
70
|
+
#: both the write side (``_configure_git_hooks_path``) and the comparison
|
|
71
|
+
#: side (``hooks_path_is_stale``), so this object really is the value's only
|
|
72
|
+
#: source.
|
|
73
|
+
#:
|
|
74
|
+
#: Absolute on purpose: the value no longer lives in the repository's shared
|
|
75
|
+
#: ``.git/config`` (where one checkout's absolute path would be invalid in
|
|
76
|
+
#: every other checkout) but in the *per-worktree* config, which is removed
|
|
77
|
+
#: together with the worktree it describes. ``as_posix()`` keeps the
|
|
78
|
+
#: forward-slash spelling git accepts on every platform.
|
|
79
|
+
worktree_hooks_path: str
|
|
80
|
+
#: Whether :attr:`template_dir` was absent when this snapshot was taken.
|
|
81
|
+
#: The explicit "template missing" result (#162): it exists so callers can
|
|
82
|
+
#: warn (or skip) instead of :func:`locate_hook_source` silently
|
|
83
|
+
#: re-resolving to another platform's directory -- the ``hooks/win``
|
|
84
|
+
#: fallback the bare ``hooks`` key used to produce on Windows.
|
|
85
|
+
template_missing: bool
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def locate_hook_source(deploy_root: Root) -> HookSource:
|
|
89
|
+
"""Locate the hook source and deployment target for *deploy_root*.
|
|
90
|
+
|
|
91
|
+
``deploy_root`` is the root the hooks deploy under, supplied as a value by
|
|
92
|
+
the caller (the root module's ``DEPLOY`` kind, resolved once at the public
|
|
93
|
+
entry) rather than re-derived here -- a default would let a forgotten
|
|
94
|
+
argument silently fall back to the process CWD (#160).
|
|
95
|
+
|
|
96
|
+
A missing template directory is *not* an error: the result carries
|
|
97
|
+
:attr:`HookSource.template_missing`, so the read-only staleness gate can
|
|
98
|
+
keep a "cannot tell, do not force a sync" verdict and the applier can warn
|
|
99
|
+
(and must not read the missing directory as "the template no longer ships
|
|
100
|
+
these hooks"). The flag is probed here, once, for the same reason every
|
|
101
|
+
other field is: the result is a snapshot. Nothing is created or written
|
|
102
|
+
here.
|
|
103
|
+
"""
|
|
104
|
+
template_dir = ConfigManager().get_vendor_config_path(_HOOK_TEMPLATE_KEY)
|
|
105
|
+
return HookSource(
|
|
106
|
+
template_dir=template_dir,
|
|
107
|
+
deploy_dir=deploy_root.hooks_dir(),
|
|
108
|
+
worktree_hooks_path=deploy_root.hooks_dir().as_posix(),
|
|
109
|
+
template_missing=not template_dir.is_dir(),
|
|
110
|
+
)
|