kanibako-cli 1.6.0.dev30__tar.gz → 1.6.0.dev31__tar.gz
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.
- {kanibako_cli-1.6.0.dev30/src/kanibako_cli.egg-info → kanibako_cli-1.6.0.dev31}/PKG-INFO +1 -1
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/pyproject.toml +1 -1
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/__init__.py +1 -1
- kanibako_cli-1.6.0.dev31/src/kanibako/box_seed.py +84 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/start.py +57 -12
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/registry_store.py +60 -1
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/templates.py +18 -48
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/workset.py +24 -1
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31/src/kanibako_cli.egg-info}/PKG-INFO +1 -1
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako_cli.egg-info/SOURCES.txt +2 -0
- kanibako_cli-1.6.0.dev31/tests/test_box_seed.py +137 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_registry_store.py +76 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_templates.py +0 -73
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_workset.py +71 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/LICENSE.md +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/README.md +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/setup.cfg +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/__main__.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/_atomic.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/agent_config.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/auth_browser.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/auth_parser.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/baseline.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/box_identity.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/browser_sidecar.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/browser_state.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/bun_sea.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/channels.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/cli.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/__init__.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/agent_cmd.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/archive.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/baseline_cmd.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/box/__init__.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/box/_duplicate.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/box/_lifecycle.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/box/_parser.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/clean.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/diagnose.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/flags.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/fork_cmd.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/helper_cmd.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/image.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/install.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/restore.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/setup_cmd.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/stop.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/system_cmd.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/upgrade.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/vault_cmd.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/commands/workset_cmd.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/config.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/config_interface.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/config_io.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/container.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/containerfiles.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/containers/Containerfile.template-android +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/containers/Containerfile.template-dotnet +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/containers/Containerfile.template-js +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/containers/Containerfile.template-jvm +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/containers/Containerfile.template-systems +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/containers/__init__.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/containers/tmux.conf +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/data/__init__.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/data/image-baseline.yaml +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/data/templates/base/INSTRUCTIONS.md +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/deprecation.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/errors.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/freshness.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/git.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/helper_client.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/helper_listener.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/helpers.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/hygiene.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/image_sharing.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/import_reconcile.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/install_method.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/log.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/names.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/paths.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/plugins/__init__.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/py.typed +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/registry.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/rig_bundle.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/rig_meta.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/rig_registry.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/rig_resolve.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/rig_source.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/scripts/__init__.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/scripts/helper-init.sh +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/scripts/kanibako-entry +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/settings_categories.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/settings_resolve.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/settings_seeds.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/settings_shares.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/shellenv.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/shells.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/snapshots.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/targets/__init__.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/targets/assembly.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/targets/base.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/targets/credsync.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/targets/no_agent.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/templates_image.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/tweakcc.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/tweakcc_cache.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako/utils.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako_cli.egg-info/dependency_links.txt +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako_cli.egg-info/entry_points.txt +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako_cli.egg-info/requires.txt +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/src/kanibako_cli.egg-info/top_level.txt +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_agent_config.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_agent_resolution.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_archive_restore_integration.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_atomic.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_auth_browser.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_auth_parser.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_baseline.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_box_identity.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_browser_sidecar.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_browser_state.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_bun_sea.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_channels.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_cli.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_cli_exitcodes.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_comms.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_config.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_config_interface.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_container.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_container_extended.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_container_integration.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_containerfiles.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_credentials.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_deprecations.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_env_cmd.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_error_messages.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_fork.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_freshness.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_git.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_helper_client.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_helper_cmd.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_helper_listener.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_helpers.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_hygiene.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_image_sharing.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_import.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_init_cmd.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_install_integration.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_lifecycle_integration.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_log.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_names.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_paths.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_paths_extended.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_registry.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_resolve_box_target.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_resource_scoping.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_rig_bundle.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_rig_meta.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_rig_registry.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_rig_resolve.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_rig_source.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_safety_invariants.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_settings_categories.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_settings_loader.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_settings_resolve.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_settings_seeds.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_settings_shares.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_setup_cmd.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_shellenv.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_shells.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_snapshots.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_standalone_paths.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_start_integration.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_status.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_system_paths.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_templates_image.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_tweakcc.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_utils.py +0 -0
- {kanibako_cli-1.6.0.dev30 → kanibako_cli-1.6.0.dev31}/tests/test_workset_paths.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kanibako-cli"
|
|
7
|
-
version = "1.6.0.
|
|
7
|
+
version = "1.6.0.dev31"
|
|
8
8
|
description = "Run AI coding agents in rootless containers with per-project isolation"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
dependencies = ["argcomplete>=3.0", "PyYAML>=6.0", "packaging>=20.0"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""kanibako: Run AI coding agents in rootless containers with per-project isolation."""
|
|
2
2
|
|
|
3
|
-
__version__ = "1.6.0.
|
|
3
|
+
__version__ = "1.6.0.dev31"
|
|
4
4
|
|
|
5
5
|
# The oldest build version whose ``setup`` run is still considered current. The
|
|
6
6
|
# setup-completion gate (W1) re-nudges the user to re-run ``kanibako setup`` when
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Uniform per-box seed-once query/record API.
|
|
2
|
+
|
|
3
|
+
This is the single read/write surface the launch path (Step 3) will call to
|
|
4
|
+
ask "has this box already been seeded?" and to record "this box is now
|
|
5
|
+
seeded". It hides where each box's seeded flag lives by dispatching on
|
|
6
|
+
``proj.mode`` (a :class:`kanibako.paths.BoxMode`) to whichever registry owns
|
|
7
|
+
the box:
|
|
8
|
+
|
|
9
|
+
* ``BoxMode.primary`` → the ``seeded`` section's ``projects`` domain in
|
|
10
|
+
``registry.yaml`` (keyed by ``proj.name``).
|
|
11
|
+
* ``BoxMode.standalone`` → the ``seeded`` section's ``standalone`` domain in
|
|
12
|
+
``registry.yaml`` (keyed by ``proj.name``).
|
|
13
|
+
* ``BoxMode.named`` → ``WorksetProject.seeded`` inside the owning
|
|
14
|
+
workset's ``settings.yaml`` (located via ``proj.group.root``).
|
|
15
|
+
|
|
16
|
+
It is the explicit, registry-resident successor to the brittle ``.seeded``
|
|
17
|
+
sentinel file (now REMOVED): Step 3 wired launch-time detection over to here,
|
|
18
|
+
ORing this registry flag with an existing-inbox backstop for legacy boxes.
|
|
19
|
+
Layering: this module
|
|
20
|
+
depends on ``paths`` (types only), ``registry_store``, and ``workset``; none of
|
|
21
|
+
those import ``box_seed``, so there is no import cycle.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from kanibako import registry_store
|
|
27
|
+
from kanibako.paths import BoxMode, ProjectPaths, StandardPaths
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def box_is_seeded(proj: ProjectPaths, std: StandardPaths) -> bool:
|
|
31
|
+
"""Return whether *proj*'s box has completed its one-time seed.
|
|
32
|
+
|
|
33
|
+
Dispatches on ``proj.mode`` to the owning registry. Returns False for a
|
|
34
|
+
box that has never been seeded (or, defensively, a ``named`` box whose
|
|
35
|
+
group/workset-project cannot be resolved).
|
|
36
|
+
"""
|
|
37
|
+
if proj.mode is BoxMode.primary:
|
|
38
|
+
return registry_store.is_box_seeded(std.data_path, "projects", proj.name)
|
|
39
|
+
if proj.mode is BoxMode.standalone:
|
|
40
|
+
return registry_store.is_box_seeded(std.data_path, "standalone", proj.name)
|
|
41
|
+
# BoxMode.named: the flag lives on the WorksetProject.
|
|
42
|
+
if proj.group is None:
|
|
43
|
+
return False
|
|
44
|
+
from kanibako.errors import WorksetError
|
|
45
|
+
from kanibako.workset import load_workset
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
ws = load_workset(proj.group.root)
|
|
49
|
+
except WorksetError:
|
|
50
|
+
# Defensive: a named box whose workset cannot be loaded (e.g. its
|
|
51
|
+
# settings.yaml is absent or not yet materialised) reads as unseeded.
|
|
52
|
+
return False
|
|
53
|
+
for p in ws.projects:
|
|
54
|
+
if p.name == proj.name:
|
|
55
|
+
return p.seeded
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def mark_box_seeded(proj: ProjectPaths, std: StandardPaths) -> None:
|
|
60
|
+
"""Record that *proj*'s box has completed its one-time seed.
|
|
61
|
+
|
|
62
|
+
Dispatches on ``proj.mode`` to the owning registry. Idempotent; a no-op
|
|
63
|
+
(defensive) for a ``named`` box whose group/workset-project cannot be
|
|
64
|
+
resolved.
|
|
65
|
+
"""
|
|
66
|
+
if proj.mode is BoxMode.primary:
|
|
67
|
+
registry_store.mark_box_seeded_entry(std.data_path, "projects", proj.name)
|
|
68
|
+
return
|
|
69
|
+
if proj.mode is BoxMode.standalone:
|
|
70
|
+
registry_store.mark_box_seeded_entry(std.data_path, "standalone", proj.name)
|
|
71
|
+
return
|
|
72
|
+
# BoxMode.named: persist on the WorksetProject via the workset's public API.
|
|
73
|
+
if proj.group is None:
|
|
74
|
+
return
|
|
75
|
+
from kanibako.errors import WorksetError
|
|
76
|
+
from kanibako.workset import load_workset, set_project_seeded
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
ws = load_workset(proj.group.root)
|
|
80
|
+
set_project_seeded(ws, proj.name)
|
|
81
|
+
except WorksetError:
|
|
82
|
+
# Defensive: a named box whose workset cannot be loaded/updated is a
|
|
83
|
+
# no-op (mirrors the missing-group guard above).
|
|
84
|
+
return
|
|
@@ -37,6 +37,31 @@ from kanibako.targets import assembly, credsync, resolve_target
|
|
|
37
37
|
from kanibako.targets.assembly import BindingSourceError, descriptor_mounts
|
|
38
38
|
from kanibako.utils import container_name_for, project_hash, short_hash
|
|
39
39
|
|
|
40
|
+
from typing import TYPE_CHECKING
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from kanibako.paths import ProjectPaths, StandardPaths
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _box_already_seeded(proj: ProjectPaths, std: StandardPaths) -> bool:
|
|
47
|
+
"""True when *proj*'s box has already had its one-time home seed applied.
|
|
48
|
+
|
|
49
|
+
Two ORed signals decide this:
|
|
50
|
+
|
|
51
|
+
* the authoritative per-box ``seeded`` registry flag
|
|
52
|
+
(:func:`kanibako.box_seed.box_is_seeded`); and
|
|
53
|
+
* the backstop that the box's own mailbox (inbox) dir already exists
|
|
54
|
+
— that dir is created during launch AFTER the seed gate, so on a first
|
|
55
|
+
launch it is absent (→ seed) and on every later launch it is present
|
|
56
|
+
(→ skip). The inbox backstop also covers LEGACY boxes created before
|
|
57
|
+
the registry flag existed.
|
|
58
|
+
"""
|
|
59
|
+
from kanibako import box_seed, channels
|
|
60
|
+
|
|
61
|
+
if box_seed.box_is_seeded(proj, std):
|
|
62
|
+
return True
|
|
63
|
+
return channels.box_channel_addresses(proj, std).inbox.exists()
|
|
64
|
+
|
|
40
65
|
|
|
41
66
|
def add_start_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
42
67
|
p = subparsers.add_parser(
|
|
@@ -902,13 +927,28 @@ def _run_container(
|
|
|
902
927
|
logger.info(action)
|
|
903
928
|
|
|
904
929
|
# Seed-once gate (BUG-D): seed the box home exactly once, on its FIRST
|
|
905
|
-
# start — whether the box was created by this very `start`
|
|
906
|
-
#
|
|
907
|
-
#
|
|
908
|
-
#
|
|
909
|
-
#
|
|
910
|
-
|
|
911
|
-
|
|
930
|
+
# start — whether the box was created by this very `start` or earlier by
|
|
931
|
+
# `box create`. Detection ORs two signals (see `_box_already_seeded`):
|
|
932
|
+
# the authoritative per-box `seeded` registry flag, plus a backstop that
|
|
933
|
+
# the box's mailbox (inbox) already exists. The inbox dir is created
|
|
934
|
+
# later in launch (after this gate), so it is absent on a first start
|
|
935
|
+
# (→ seed) and present on every later start (→ skip), and it also covers
|
|
936
|
+
# LEGACY boxes that predate the registry flag. This replaces the brittle
|
|
937
|
+
# `.seeded` sentinel file (removed); the non-destructive create-if-absent
|
|
938
|
+
# template/seed application (S1) is the failsafe so a mis-detection can
|
|
939
|
+
# never clobber user edits.
|
|
940
|
+
from kanibako.box_seed import box_is_seeded, mark_box_seeded
|
|
941
|
+
flag_seeded = box_is_seeded(proj, std)
|
|
942
|
+
already_seeded = _box_already_seeded(proj, std)
|
|
943
|
+
seed_box = not already_seeded
|
|
944
|
+
# ADOPT-ON-DETECT: a legacy box detected seeded only via its existing
|
|
945
|
+
# inbox (flag still False) gets the authoritative flag stamped once, so
|
|
946
|
+
# future detection uses the flag. Cheap + idempotent; never fatal.
|
|
947
|
+
if not flag_seeded and already_seeded:
|
|
948
|
+
try:
|
|
949
|
+
mark_box_seeded(proj, std)
|
|
950
|
+
except Exception: # pragma: no cover - adopt is best-effort
|
|
951
|
+
logger.debug("adopt-on-detect: could not stamp seeded flag", exc_info=True)
|
|
912
952
|
|
|
913
953
|
# Template application + agent init for new projects.
|
|
914
954
|
if seed_box:
|
|
@@ -948,10 +988,11 @@ def _run_container(
|
|
|
948
988
|
logger=logger, group_auth=proj.group_auth,
|
|
949
989
|
)
|
|
950
990
|
|
|
951
|
-
# Record seed-once completion
|
|
952
|
-
# above (idempotent; only the
|
|
991
|
+
# Record seed-once completion via the authoritative per-box registry
|
|
992
|
+
# flag so subsequent launches skip the blocks above (idempotent; only the
|
|
993
|
+
# first start writes meaningful content).
|
|
953
994
|
if seed_box:
|
|
954
|
-
|
|
995
|
+
mark_box_seeded(proj, std)
|
|
955
996
|
|
|
956
997
|
# Synced copies (the `<scope>.synced.<name>` category) — applied on
|
|
957
998
|
# EVERY launch (mtime-gated), unlike copy-once seeds. Distinct from the
|
|
@@ -2010,6 +2051,7 @@ def _apply_init_seeds(
|
|
|
2010
2051
|
import shutil
|
|
2011
2052
|
|
|
2012
2053
|
from kanibako.settings_resolve import GUEST_HOME
|
|
2054
|
+
from kanibako.templates import copy_resource_tree_if_absent
|
|
2013
2055
|
|
|
2014
2056
|
default_seeds = target.default_seeds() if target is not None else {}
|
|
2015
2057
|
|
|
@@ -2050,8 +2092,11 @@ def _apply_init_seeds(
|
|
|
2050
2092
|
continue
|
|
2051
2093
|
if src.is_dir():
|
|
2052
2094
|
dest.mkdir(parents=True, exist_ok=True)
|
|
2053
|
-
|
|
2054
|
-
|
|
2095
|
+
# Create-if-absent: a seed delivers content ONCE; existing home
|
|
2096
|
+
# content is owned by the box and must never be overwritten by a
|
|
2097
|
+
# re-seed (the playbook-clobber bug).
|
|
2098
|
+
copy_resource_tree_if_absent(src, dest)
|
|
2099
|
+
elif not dest.exists():
|
|
2055
2100
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
2056
2101
|
shutil.copy2(str(src), str(dest))
|
|
2057
2102
|
|
|
@@ -66,6 +66,7 @@ _SECTIONS: tuple[str, ...] = (
|
|
|
66
66
|
"workset_roots",
|
|
67
67
|
"connected",
|
|
68
68
|
"standalone",
|
|
69
|
+
"seeded",
|
|
69
70
|
"rigs",
|
|
70
71
|
"image_shells",
|
|
71
72
|
)
|
|
@@ -95,6 +96,9 @@ def load_registry(data_path: Path) -> dict[str, dict]:
|
|
|
95
96
|
"""
|
|
96
97
|
path = registry_path(data_path)
|
|
97
98
|
data = load_doc(path) if path.is_file() else {}
|
|
99
|
+
seeded_raw = data.get("seeded", {})
|
|
100
|
+
if not isinstance(seeded_raw, dict):
|
|
101
|
+
seeded_raw = {}
|
|
98
102
|
return {
|
|
99
103
|
"projects": {
|
|
100
104
|
k: str(v) for k, v in dict(data.get("projects", {})).items()
|
|
@@ -107,6 +111,16 @@ def load_registry(data_path: Path) -> dict[str, dict]:
|
|
|
107
111
|
},
|
|
108
112
|
"connected": dict(data.get("connected", {})),
|
|
109
113
|
"standalone": dict(data.get("standalone", {})),
|
|
114
|
+
"seeded": {
|
|
115
|
+
"projects": {
|
|
116
|
+
k: bool(v)
|
|
117
|
+
for k, v in dict(seeded_raw.get("projects", {})).items()
|
|
118
|
+
},
|
|
119
|
+
"standalone": {
|
|
120
|
+
k: bool(v)
|
|
121
|
+
for k, v in dict(seeded_raw.get("standalone", {})).items()
|
|
122
|
+
},
|
|
123
|
+
},
|
|
110
124
|
"rigs": dict(data.get("rigs", {})),
|
|
111
125
|
"image_shells": dict(data.get("image_shells", {})),
|
|
112
126
|
}
|
|
@@ -122,7 +136,17 @@ def save_registry(data_path: Path, registry: dict[str, dict]) -> None:
|
|
|
122
136
|
data: dict = {}
|
|
123
137
|
for section in _SECTIONS:
|
|
124
138
|
entries = registry.get(section, {}) or {}
|
|
125
|
-
if section
|
|
139
|
+
if section == "seeded":
|
|
140
|
+
# Nested ``{domain: {name: bool}}`` shape (NOT a name->path map):
|
|
141
|
+
# persist each domain with its inner keys sorted for stable diffs.
|
|
142
|
+
data[section] = {
|
|
143
|
+
domain: {
|
|
144
|
+
name: bool(entries.get(domain, {})[name])
|
|
145
|
+
for name in sorted(entries.get(domain, {}))
|
|
146
|
+
}
|
|
147
|
+
for domain in ("projects", "standalone")
|
|
148
|
+
}
|
|
149
|
+
elif section in _NAME_SECTIONS:
|
|
126
150
|
data[section] = {name: entries[name] for name in sorted(entries)}
|
|
127
151
|
else:
|
|
128
152
|
data[section] = dict(entries)
|
|
@@ -224,3 +248,38 @@ def standalone_name_for_root(data_path: Path, root: Path) -> str | None:
|
|
|
224
248
|
if root_str == target:
|
|
225
249
|
return name
|
|
226
250
|
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
# Seeded-flag helpers (``seeded`` section: {domain: {box_name: bool}})
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
#
|
|
257
|
+
# These are the per-box seed-once read/write primitives for the PRIMARY
|
|
258
|
+
# (``domain == "projects"``) and STANDALONE (``domain == "standalone"``)
|
|
259
|
+
# registries, the explicit successor to the brittle ``.seeded`` sentinel file.
|
|
260
|
+
# The named-workset seeded flag lives on ``WorksetProject.seeded`` instead.
|
|
261
|
+
# The uniform launch-path API that dispatches across all three lives in
|
|
262
|
+
# :mod:`kanibako.box_seed`.
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def is_box_seeded(data_path: Path, domain: str, box_name: str) -> bool:
|
|
266
|
+
"""Return whether *box_name* in *domain* has completed its one-time seed.
|
|
267
|
+
|
|
268
|
+
*domain* is ``"projects"`` (PRIMARY) or ``"standalone"``. False when the
|
|
269
|
+
section/domain/name is absent (a fresh or legacy registry).
|
|
270
|
+
"""
|
|
271
|
+
return bool(
|
|
272
|
+
load_registry(data_path)["seeded"].get(domain, {}).get(box_name, False)
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def mark_box_seeded_entry(data_path: Path, domain: str, box_name: str) -> None:
|
|
277
|
+
"""Record that *box_name* in *domain* has completed its one-time seed.
|
|
278
|
+
|
|
279
|
+
Idempotent; preserves every sibling section (whole-file rewrite via
|
|
280
|
+
``save_registry``). Reuses ``load_registry``/``save_registry`` — no extra
|
|
281
|
+
YAML I/O.
|
|
282
|
+
"""
|
|
283
|
+
registry = load_registry(data_path)
|
|
284
|
+
registry["seeded"].setdefault(domain, {})[box_name] = True
|
|
285
|
+
save_registry(data_path, registry)
|
|
@@ -62,44 +62,6 @@ def workset_template_dir(proj: ProjectPaths, std: StandardPaths) -> Path | None:
|
|
|
62
62
|
return workset_root(proj, std) / "template"
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
#: Sentinel filename (under the box metadata dir) recording that the seed-once
|
|
66
|
-
#: home content (templates + credential seeds + init seeds) has been applied.
|
|
67
|
-
#: Its presence — NOT ``proj.is_new`` — is what gates seed-once application, so a
|
|
68
|
-
#: box made via the two-step ``box create`` then ``start`` flow (which creates
|
|
69
|
-
#: the metadata dir at CREATE time, leaving ``is_new`` False at START) is still
|
|
70
|
-
#: seeded exactly once on its first ``start`` (BUG-D).
|
|
71
|
-
SEED_MARKER = ".seeded"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def seed_marker_path(metadata_path: Path) -> Path:
|
|
75
|
-
"""Return the seed-once sentinel path under *metadata_path*."""
|
|
76
|
-
return metadata_path / SEED_MARKER
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def needs_seed(metadata_path: Path, *, is_new: bool) -> bool:
|
|
80
|
-
"""True when the box still needs its one-time home seeding.
|
|
81
|
-
|
|
82
|
-
A box needs seeding when it is freshly created (*is_new*) OR when it was
|
|
83
|
-
created earlier (e.g. via ``box create``) but never seeded — detected by the
|
|
84
|
-
absence of the :data:`SEED_MARKER` sentinel under *metadata_path*. Once the
|
|
85
|
-
sentinel exists the box is never re-seeded, so user edits to seeded files
|
|
86
|
-
survive subsequent launches (D-B6).
|
|
87
|
-
"""
|
|
88
|
-
if is_new:
|
|
89
|
-
return True
|
|
90
|
-
return not seed_marker_path(metadata_path).exists()
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def mark_seeded(metadata_path: Path) -> None:
|
|
94
|
-
"""Record that the box's one-time home seeding has completed.
|
|
95
|
-
|
|
96
|
-
Writes the :data:`SEED_MARKER` sentinel under *metadata_path* so a later
|
|
97
|
-
launch's :func:`needs_seed` returns False. Idempotent.
|
|
98
|
-
"""
|
|
99
|
-
metadata_path.mkdir(parents=True, exist_ok=True)
|
|
100
|
-
seed_marker_path(metadata_path).touch()
|
|
101
|
-
|
|
102
|
-
|
|
103
65
|
def apply_template_layers(
|
|
104
66
|
home: Path,
|
|
105
67
|
layers: list[Path | None],
|
|
@@ -109,10 +71,11 @@ def apply_template_layers(
|
|
|
109
71
|
The 1.6.0 home-seed model layers ordered template sources into the box home
|
|
110
72
|
at creation (base -> agent -> workset; later overlays earlier). The copy is
|
|
111
73
|
NON-DESTRUCTIVE toward files that ALREADY EXIST in *home* before this seed
|
|
112
|
-
pass begins: such a pre-existing file (a user-edited home file on a
|
|
113
|
-
|
|
114
|
-
re-seed DATA LOSS — a box whose home is already populated but
|
|
115
|
-
|
|
74
|
+
pass begins: such a pre-existing file (a user-edited home file on a box that
|
|
75
|
+
detection mis-flags as un-seeded) is NEVER clobbered. This is the guard
|
|
76
|
+
against re-seed DATA LOSS — a box whose home is already populated but which
|
|
77
|
+
detection (the per-box registry flag + inbox backstop) fails to recognise as
|
|
78
|
+
seeded would otherwise have its files overwritten.
|
|
116
79
|
|
|
117
80
|
Within a SINGLE seed pass, cross-layer LAST-WINS is still honoured: a file
|
|
118
81
|
written by an earlier layer during this pass IS overwritten by a later
|
|
@@ -122,12 +85,14 @@ def apply_template_layers(
|
|
|
122
85
|
are skipped (a ``<None>`` / absent layer contributes nothing — e.g.
|
|
123
86
|
STANDALONE boxes have no workset layer).
|
|
124
87
|
|
|
125
|
-
This is SEED-ONCE: callers invoke it only when the box still
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
88
|
+
This is SEED-ONCE: callers invoke it only when the box still needs seeding
|
|
89
|
+
(the launch gate detects an already-seeded box via the per-box registry flag
|
|
90
|
+
OR the existing-inbox backstop). It does NOT special-case or merge any file
|
|
91
|
+
— every file is a plain ordered copy (the CLAUDE.md merge special-case is
|
|
92
|
+
gone, D-B5). The caller is responsible for never re-running it once the box
|
|
93
|
+
is recorded seeded so user edits made inside the box survive (D-B6); the
|
|
94
|
+
non-destructive copy here is an additional safety net against a re-seed
|
|
95
|
+
caused by mis-detection.
|
|
131
96
|
"""
|
|
132
97
|
written: set[Path] = set()
|
|
133
98
|
for layer in layers:
|
|
@@ -189,6 +154,11 @@ def _copy_resource_tree_if_absent(src: Path, dest: Path) -> None:
|
|
|
189
154
|
shutil.copy2(str(entry), str(target))
|
|
190
155
|
|
|
191
156
|
|
|
157
|
+
# Public alias so other modules (e.g. the seed-once apply in commands.start)
|
|
158
|
+
# can reuse the create-if-absent tree copy without reaching for a private name.
|
|
159
|
+
copy_resource_tree_if_absent = _copy_resource_tree_if_absent
|
|
160
|
+
|
|
161
|
+
|
|
192
162
|
def _packaged_base_template() -> Path | None:
|
|
193
163
|
"""Locate the packaged base-template content (``kanibako.data/templates/base``)."""
|
|
194
164
|
try:
|
|
@@ -106,6 +106,7 @@ class WorksetProject:
|
|
|
106
106
|
|
|
107
107
|
name: str
|
|
108
108
|
source_path: Path # original project path (for reference / cloning)
|
|
109
|
+
seeded: bool = False # True once the box's one-time seed has completed
|
|
109
110
|
|
|
110
111
|
|
|
111
112
|
@dataclass
|
|
@@ -174,7 +175,11 @@ def _write_workset_toml(ws: Workset) -> None:
|
|
|
174
175
|
"created": ws.created,
|
|
175
176
|
"group_auth": ws.group_auth,
|
|
176
177
|
"projects": [
|
|
177
|
-
{
|
|
178
|
+
{
|
|
179
|
+
"name": proj.name,
|
|
180
|
+
"source_path": str(proj.source_path),
|
|
181
|
+
"seeded": proj.seeded,
|
|
182
|
+
}
|
|
178
183
|
for proj in ws.projects
|
|
179
184
|
],
|
|
180
185
|
}
|
|
@@ -204,6 +209,7 @@ def _load_workset_toml(root: Path) -> Workset:
|
|
|
204
209
|
WorksetProject(
|
|
205
210
|
name=entry["name"],
|
|
206
211
|
source_path=Path(entry["source_path"]),
|
|
212
|
+
seeded=bool(entry.get("seeded", False)),
|
|
207
213
|
)
|
|
208
214
|
)
|
|
209
215
|
return Workset(name=name, root=root, created=created, projects=projects, group_auth=group_auth)
|
|
@@ -673,6 +679,23 @@ def _detach_project(ws: Workset, name: str) -> None:
|
|
|
673
679
|
ws.projects[:] = [p for p in ws.projects if p.name != name]
|
|
674
680
|
|
|
675
681
|
|
|
682
|
+
def set_project_seeded(ws: Workset, name: str) -> bool:
|
|
683
|
+
"""Mark the project *name* in *ws* as seeded and persist the change.
|
|
684
|
+
|
|
685
|
+
Sets ``WorksetProject.seeded`` True for the matching project and rewrites
|
|
686
|
+
the workset identity (preserving cascade settings). Idempotent; a no-op
|
|
687
|
+
that returns False when *name* is not a project in *ws*, True otherwise.
|
|
688
|
+
This is the public persist entry point used by :mod:`kanibako.box_seed` so
|
|
689
|
+
callers do not reach for the module-private ``_write_workset_toml``.
|
|
690
|
+
"""
|
|
691
|
+
for proj in ws.projects:
|
|
692
|
+
if proj.name == name:
|
|
693
|
+
proj.seeded = True
|
|
694
|
+
_write_workset_toml(ws)
|
|
695
|
+
return True
|
|
696
|
+
return False
|
|
697
|
+
|
|
698
|
+
|
|
676
699
|
def remove_project(
|
|
677
700
|
ws: Workset, name: str, *, remove_files: bool = False,
|
|
678
701
|
std: StandardPaths | None = None,
|
|
@@ -9,6 +9,7 @@ src/kanibako/auth_browser.py
|
|
|
9
9
|
src/kanibako/auth_parser.py
|
|
10
10
|
src/kanibako/baseline.py
|
|
11
11
|
src/kanibako/box_identity.py
|
|
12
|
+
src/kanibako/box_seed.py
|
|
12
13
|
src/kanibako/browser_sidecar.py
|
|
13
14
|
src/kanibako/browser_state.py
|
|
14
15
|
src/kanibako/bun_sea.py
|
|
@@ -110,6 +111,7 @@ tests/test_auth_browser.py
|
|
|
110
111
|
tests/test_auth_parser.py
|
|
111
112
|
tests/test_baseline.py
|
|
112
113
|
tests/test_box_identity.py
|
|
114
|
+
tests/test_box_seed.py
|
|
113
115
|
tests/test_browser_sidecar.py
|
|
114
116
|
tests/test_browser_state.py
|
|
115
117
|
tests/test_bun_sea.py
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Tests for kanibako.box_seed -- the uniform per-box seed-once dispatcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from kanibako import box_seed, registry_store
|
|
8
|
+
from kanibako.paths import BoxMode, ProjectGroup, ProjectPaths
|
|
9
|
+
from kanibako.workset import add_project, create_workset, load_workset
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _make_proj(mode: BoxMode, name: str, group: ProjectGroup | None) -> ProjectPaths:
|
|
13
|
+
"""Build a minimal ProjectPaths carrying only the dispatch-relevant fields."""
|
|
14
|
+
dummy = Path("/nonexistent")
|
|
15
|
+
return ProjectPaths(
|
|
16
|
+
project_path=dummy,
|
|
17
|
+
project_hash="0" * 16,
|
|
18
|
+
metadata_path=dummy,
|
|
19
|
+
shell_path=dummy,
|
|
20
|
+
vault_ro_path=dummy,
|
|
21
|
+
vault_rw_path=dummy,
|
|
22
|
+
mode=mode,
|
|
23
|
+
name=name,
|
|
24
|
+
group=group,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _primary_group(std) -> ProjectGroup:
|
|
29
|
+
return ProjectGroup(
|
|
30
|
+
name="default",
|
|
31
|
+
root=std.data_path,
|
|
32
|
+
is_default=True,
|
|
33
|
+
local_shared_base=std.data_path,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# PRIMARY → registry seeded.projects
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_primary_routes_to_projects_domain(std, tmp_home):
|
|
43
|
+
proj = _make_proj(BoxMode.primary, "myapp", _primary_group(std))
|
|
44
|
+
|
|
45
|
+
assert box_seed.box_is_seeded(proj, std) is False
|
|
46
|
+
box_seed.mark_box_seeded(proj, std)
|
|
47
|
+
assert box_seed.box_is_seeded(proj, std) is True
|
|
48
|
+
|
|
49
|
+
# Landed in projects domain ONLY.
|
|
50
|
+
assert registry_store.is_box_seeded(std.data_path, "projects", "myapp") is True
|
|
51
|
+
assert registry_store.is_box_seeded(std.data_path, "standalone", "myapp") is False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# STANDALONE → registry seeded.standalone
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_standalone_routes_to_standalone_domain(std, tmp_home):
|
|
60
|
+
proj = _make_proj(BoxMode.standalone, "abc_box", None)
|
|
61
|
+
|
|
62
|
+
assert box_seed.box_is_seeded(proj, std) is False
|
|
63
|
+
box_seed.mark_box_seeded(proj, std)
|
|
64
|
+
assert box_seed.box_is_seeded(proj, std) is True
|
|
65
|
+
|
|
66
|
+
# Landed in standalone domain ONLY.
|
|
67
|
+
assert registry_store.is_box_seeded(std.data_path, "standalone", "abc_box") is True
|
|
68
|
+
assert registry_store.is_box_seeded(std.data_path, "projects", "abc_box") is False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# NAMED → WorksetProject.seeded
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_named_routes_to_workset_project(std, tmp_home):
|
|
77
|
+
ws = create_workset("myset", tmp_home / "worksets" / "myset", std)
|
|
78
|
+
add_project(ws, "projname", tmp_home / "project")
|
|
79
|
+
|
|
80
|
+
group = ProjectGroup(
|
|
81
|
+
name=ws.name,
|
|
82
|
+
root=ws.root,
|
|
83
|
+
is_default=False,
|
|
84
|
+
local_shared_base=ws.root,
|
|
85
|
+
)
|
|
86
|
+
proj = _make_proj(BoxMode.named, "projname", group)
|
|
87
|
+
|
|
88
|
+
assert box_seed.box_is_seeded(proj, std) is False
|
|
89
|
+
box_seed.mark_box_seeded(proj, std)
|
|
90
|
+
assert box_seed.box_is_seeded(proj, std) is True
|
|
91
|
+
|
|
92
|
+
# Landed on the WorksetProject, NOT in either registry domain.
|
|
93
|
+
assert load_workset(ws.root).projects[0].seeded is True
|
|
94
|
+
assert registry_store.is_box_seeded(std.data_path, "projects", "projname") is False
|
|
95
|
+
assert (
|
|
96
|
+
registry_store.is_box_seeded(std.data_path, "standalone", "projname") is False
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_named_missing_group_is_defensive(std, tmp_home):
|
|
101
|
+
"""A named box with no group reads unseeded and mark is a no-op."""
|
|
102
|
+
proj = _make_proj(BoxMode.named, "orphan", None)
|
|
103
|
+
assert box_seed.box_is_seeded(proj, std) is False
|
|
104
|
+
box_seed.mark_box_seeded(proj, std) # must not raise
|
|
105
|
+
assert box_seed.box_is_seeded(proj, std) is False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_named_unloadable_workset_is_defensive(std, tmp_home):
|
|
109
|
+
"""A named box whose workset root has no settings.yaml reads unseeded.
|
|
110
|
+
|
|
111
|
+
The seed gate calls ``box_is_seeded``/``mark_box_seeded`` early in launch; a
|
|
112
|
+
named box whose workset cannot be loaded (e.g. a not-yet-materialised root)
|
|
113
|
+
must NOT raise — it reads as unseeded and the mark is a no-op.
|
|
114
|
+
"""
|
|
115
|
+
bare_root = tmp_home / "no-such-workset"
|
|
116
|
+
bare_root.mkdir()
|
|
117
|
+
group = ProjectGroup(
|
|
118
|
+
name="ghostset", root=bare_root, is_default=False, local_shared_base=bare_root,
|
|
119
|
+
)
|
|
120
|
+
proj = _make_proj(BoxMode.named, "projname", group)
|
|
121
|
+
|
|
122
|
+
assert box_seed.box_is_seeded(proj, std) is False # must not raise
|
|
123
|
+
box_seed.mark_box_seeded(proj, std) # must not raise
|
|
124
|
+
assert box_seed.box_is_seeded(proj, std) is False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_marking_one_mode_leaves_others_empty(std, tmp_home):
|
|
128
|
+
"""Marking the primary box does not touch the standalone domain or worksets."""
|
|
129
|
+
ws = create_workset("myset", tmp_home / "worksets" / "myset", std)
|
|
130
|
+
add_project(ws, "shared", tmp_home / "project")
|
|
131
|
+
|
|
132
|
+
primary = _make_proj(BoxMode.primary, "shared", _primary_group(std))
|
|
133
|
+
box_seed.mark_box_seeded(primary, std)
|
|
134
|
+
|
|
135
|
+
# Standalone domain untouched; the same-named workset project unseeded.
|
|
136
|
+
assert registry_store.is_box_seeded(std.data_path, "standalone", "shared") is False
|
|
137
|
+
assert load_workset(ws.root).projects[0].seeded is False
|