agent-notes 2.8.0__tar.gz → 2.10.0__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.
- {agent_notes-2.8.0 → agent_notes-2.10.0}/PKG-INFO +1 -1
- agent_notes-2.10.0/agent_notes/VERSION +1 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/commands/wizard.py +30 -1
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/fs.py +18 -22
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/installer.py +156 -1
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes.egg-info/PKG-INFO +1 -1
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes.egg-info/SOURCES.txt +3 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/conftest.py +11 -7
- agent_notes-2.10.0/tests/unit/commands/test_wizard_preflight.py +232 -0
- agent_notes-2.10.0/tests/unit/services/test_fs.py +192 -0
- agent_notes-2.10.0/tests/unit/services/test_installer_plan.py +293 -0
- agent_notes-2.8.0/agent_notes/VERSION +0 -1
- {agent_notes-2.8.0 → agent_notes-2.10.0}/LICENSE +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/README.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/__main__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/cli.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/commands/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/commands/_install_helpers.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/commands/build.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/commands/config.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/commands/doctor.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/commands/info.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/commands/install.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/commands/list.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/commands/memory.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/commands/regenerate.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/commands/set_role.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/commands/uninstall.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/commands/update.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/commands/validate.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/config.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/agents.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/analyst.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/api-reviewer.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/architect.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/coder.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/database-specialist.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/debugger.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/devil.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/devops.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/explorer.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/integrations.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/lead.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/performance-profiler.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/refactorer.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/reviewer.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/security-auditor.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/shared/cost_reporting.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/system-auditor.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/tech-writer.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/test-runner.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/agents/test-writer.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/cli/claude.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/cli/copilot.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/cli/opencode.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/commands/brainstorm.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/commands/debug.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/commands/review.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/global-claude.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/global-copilot.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/global-opencode.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/hooks/session-context.md.tpl +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/models/claude-haiku-4-5.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/models/claude-opus-4-1.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/models/claude-opus-4-5.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/models/claude-opus-4-6.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/models/claude-opus-4-7.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/models/claude-sonnet-4-5.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/models/claude-sonnet-4-6.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/models/claude-sonnet-4.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/plugin/claude.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/plugin/opencode-index.js.template +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/plugin/opencode.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/pricing.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/roles/orchestrator.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/roles/reasoner.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/roles/scout.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/roles/worker.yaml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/rules/code-quality.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/rules/safety.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/brainstorming/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/caveman/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/code-review/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/debugging-protocol/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/docker-compose/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/docker-compose-advanced/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/docker-dockerfile/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/git/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/grill-me/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/grill-with-docs/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/improve-codebase-architecture/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/obsidian-memory/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-active-storage/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-broadcasting/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-concerns/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-controllers/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-controllers-advanced/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-helpers/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-initializers/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-javascript/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-jobs/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-kamal/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-lib/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-mailers/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-migrations/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-models/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-models-advanced/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-routes/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-style/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-testing-controllers/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-testing-models/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-testing-system/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-validations/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-view-components/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-view-components-advanced/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-views/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-views-advanced/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/refactoring-protocol/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/setup-project-context/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/tdd/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/skills/zoom-out/SKILL.md +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/templates/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/templates/frontmatter/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/templates/frontmatter/claude.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/data/templates/frontmatter/opencode.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/doctor_checks.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/domain/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/domain/agent.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/domain/cli_backend.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/domain/diagnostics.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/domain/diff.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/domain/model.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/domain/role.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/domain/rule.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/domain/skill.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/domain/state.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/install_state.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/registries/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/registries/_base.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/registries/agent_registry.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/registries/cli_registry.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/registries/model_registry.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/registries/role_registry.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/registries/rule_registry.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/registries/skill_registry.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/scripts/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/scripts/_claude_backend.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/scripts/_formatting.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/scripts/_opencode_backend.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/scripts/_pricing.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/scripts/cost_report.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/credentials.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/diagnostics/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/diagnostics/_checks.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/diagnostics/_display.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/diagnostics/_fix.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/diff.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/install_state_builder.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/memory_backend.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/rendering.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/session_context.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/settings_writer.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/state_store.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/ui.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/user_config.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/services/validation.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes/state.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes.egg-info/dependency_links.txt +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes.egg-info/entry_points.txt +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes.egg-info/requires.txt +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/agent_notes.egg-info/top_level.txt +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/pyproject.toml +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/setup.cfg +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/functional/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/functional/commands/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/functional/commands/test_config_command.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/functional/commands/test_doctor_command.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/functional/commands/test_info_command.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/functional/commands/test_install_command.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/functional/commands/test_list_command.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/functional/commands/test_regenerate_command.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/functional/commands/test_uninstall_command.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/functional/commands/test_update_command.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/functional/commands/test_validate_command.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/functional/memory/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/functional/memory/test_memory_command.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/functional/scripts/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/functional/scripts/test_release_script.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/integration/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/integration/build_output/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/integration/build_output/test_build_output.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/integration/install/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/integration/install/test_install_methods.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/integration/plugin_builders/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/integration/plugin_builders/test_plugin_builders.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/plugins/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/plugins/claude/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/plugins/claude/test_agents.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/plugins/test_skills.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/commands/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/commands/test_cost_report_subcommand.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/commands/test_count_agents.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/commands/test_wizard_orchestrator_skip.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/registries/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/registries/test_registries.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/scripts/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/scripts/test_cost_report.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/scripts/test_cost_report_scoping.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/scripts/test_formatting_tty.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/scripts/test_time_aggregation.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/services/__init__.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/services/test_build_functions.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/services/test_credentials.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/services/test_memory_backend.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/services/test_memory_backend_io.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/services/test_rendering_includes.py +0 -0
- {agent_notes-2.8.0 → agent_notes-2.10.0}/tests/unit/services/test_settings_writer.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.10.0
|
|
@@ -458,9 +458,11 @@ def _select_memory(step: int, total: int, version: str = '') -> tuple:
|
|
|
458
458
|
|
|
459
459
|
|
|
460
460
|
def _confirm_install(clis: Set[str], scope: str, copy_mode: bool, selected_skills: List[str], role_models: Dict[str, Dict[str, str]], version: str = '', memory_backend: str = 'local', memory_path: str = '') -> bool:
|
|
461
|
-
"""Step 7: Confirmation."""
|
|
461
|
+
"""Step 7: Confirmation — shows pre-flight summary including files to be backed up."""
|
|
462
|
+
import logging
|
|
462
463
|
from ..services.ui import _clear_screen, _render_step_header
|
|
463
464
|
from ..registries.cli_registry import load_registry
|
|
465
|
+
from ..services.installer import plan_install
|
|
464
466
|
_clear_screen()
|
|
465
467
|
_render_step_header(7, 7, version)
|
|
466
468
|
skill_groups = _get_skill_groups()
|
|
@@ -469,6 +471,33 @@ def _confirm_install(clis: Set[str], scope: str, copy_mode: bool, selected_skill
|
|
|
469
471
|
_render_install_summary(clis, scope, copy_mode, selected_skills, role_models, skill_groups, registry,
|
|
470
472
|
memory_backend=memory_backend, memory_path=memory_path)
|
|
471
473
|
|
|
474
|
+
# Pre-flight: show which existing files will be backed up
|
|
475
|
+
_tty = sys.stdout.isatty()
|
|
476
|
+
_YELLOW = "\033[0;33m" if _tty else ""
|
|
477
|
+
_DIM = "\033[2m" if _tty else ""
|
|
478
|
+
_NC = "\033[0m" if _tty else ""
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
manifest = plan_install(
|
|
482
|
+
scope=scope,
|
|
483
|
+
registry=registry,
|
|
484
|
+
selected_clis=set(clis),
|
|
485
|
+
selected_skills=selected_skills if selected_skills else None,
|
|
486
|
+
copy_mode=copy_mode,
|
|
487
|
+
)
|
|
488
|
+
overwrites = [a for a in manifest if a.action == "overwrite"]
|
|
489
|
+
to_install = [a for a in manifest if a.action != "skip"]
|
|
490
|
+
|
|
491
|
+
print(f" {_DIM}Files to install:{_NC} {len(to_install)}")
|
|
492
|
+
if overwrites:
|
|
493
|
+
print(f" {_YELLOW}Files to back up ({len(overwrites)}):{_NC}")
|
|
494
|
+
for a in overwrites:
|
|
495
|
+
print(f" {_DIM}{a.dst}{_NC} → {a.backup_path}")
|
|
496
|
+
print("")
|
|
497
|
+
except Exception:
|
|
498
|
+
# Pre-flight is best-effort — log at debug and continue
|
|
499
|
+
logging.getLogger(__name__).debug("plan_install failed during pre-flight", exc_info=True)
|
|
500
|
+
|
|
472
501
|
choice = _safe_input("Proceed? [Y/n]: ", "Y").lower()
|
|
473
502
|
return choice != "n"
|
|
474
503
|
|
|
Binary file
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import shutil
|
|
4
4
|
import sys
|
|
5
|
+
from datetime import datetime, timezone
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Optional
|
|
7
8
|
|
|
@@ -68,35 +69,30 @@ def files_identical(a: Path, b: Path) -> bool:
|
|
|
68
69
|
return False
|
|
69
70
|
|
|
70
71
|
|
|
72
|
+
def _timestamped_backup_path(dst: Path) -> Path:
|
|
73
|
+
"""Return a timestamped backup path for dst, e.g. CLAUDE.md.bak.20260430T022500123456Z."""
|
|
74
|
+
ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
|
|
75
|
+
return Path(str(dst) + f".bak.{ts}")
|
|
76
|
+
|
|
77
|
+
|
|
71
78
|
def handle_existing(src: Path, dst: Path) -> bool:
|
|
72
79
|
"""Handle an existing non-symlink destination file.
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
|
|
81
|
+
Backs up the destination with a timestamped name and proceeds with install.
|
|
82
|
+
Returns True if install should proceed, False to skip (identical content).
|
|
75
83
|
"""
|
|
76
84
|
if files_identical(src, dst):
|
|
77
85
|
_skipped(str(dst), "exists, identical content")
|
|
78
86
|
return False
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if response == 'b':
|
|
85
|
-
backup_path = Path(str(dst) + ".bak")
|
|
86
|
-
if dst.is_dir():
|
|
87
|
-
if backup_path.exists():
|
|
88
|
-
shutil.rmtree(backup_path)
|
|
89
|
-
shutil.copytree(dst, backup_path)
|
|
90
|
-
shutil.rmtree(dst)
|
|
91
|
-
else:
|
|
92
|
-
if backup_path.exists():
|
|
93
|
-
backup_path.unlink()
|
|
94
|
-
dst.rename(backup_path)
|
|
95
|
-
print(f" {_Color.CYAN}BACKUP{_Color.NC} {backup_path}")
|
|
96
|
-
return True
|
|
87
|
+
|
|
88
|
+
backup_path = _timestamped_backup_path(dst)
|
|
89
|
+
if dst.is_dir():
|
|
90
|
+
shutil.copytree(dst, backup_path)
|
|
91
|
+
shutil.rmtree(dst)
|
|
97
92
|
else:
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
dst.rename(backup_path)
|
|
94
|
+
print(f" {_Color.CYAN}BACKUP{_Color.NC} {backup_path}")
|
|
95
|
+
return True
|
|
100
96
|
|
|
101
97
|
|
|
102
98
|
def place_file(src: Path, dst: Path, copy_mode: bool = False) -> None:
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Optional
|
|
6
|
+
from typing import List, NamedTuple, Optional
|
|
7
7
|
|
|
8
8
|
from ..domain.cli_backend import CLIBackend
|
|
9
9
|
from ..registries.cli_registry import CLIRegistry, load_registry
|
|
@@ -11,9 +11,19 @@ from .. import config
|
|
|
11
11
|
from .fs import (
|
|
12
12
|
place_file, place_dir_contents,
|
|
13
13
|
remove_symlink, remove_all_symlinks_in_dir, remove_dir_if_empty,
|
|
14
|
+
files_identical, _timestamped_backup_path,
|
|
14
15
|
)
|
|
15
16
|
from .state_store import load_state, get_scope
|
|
16
17
|
|
|
18
|
+
|
|
19
|
+
class InstallAction(NamedTuple):
|
|
20
|
+
"""Describes a single file placement that plan_install would perform."""
|
|
21
|
+
|
|
22
|
+
action: str # "install", "overwrite", or "skip"
|
|
23
|
+
src: Path
|
|
24
|
+
dst: Path
|
|
25
|
+
backup_path: Optional[Path] # set when action == "overwrite"
|
|
26
|
+
|
|
17
27
|
# Re-import the atomic helpers from install (they stay in install.py):
|
|
18
28
|
# We intentionally avoid circular import by lazy-importing inside functions.
|
|
19
29
|
|
|
@@ -144,6 +154,151 @@ def uninstall_component_for_backend(
|
|
|
144
154
|
remove_dir_if_empty(dst)
|
|
145
155
|
|
|
146
156
|
|
|
157
|
+
def _plan_file(src: Path, dst: Path, copy_mode: bool = False) -> InstallAction:
|
|
158
|
+
"""Return the InstallAction for a single src→dst placement."""
|
|
159
|
+
if copy_mode and dst.is_symlink() and dst.resolve() == src.resolve():
|
|
160
|
+
return InstallAction(action="skip", src=src, dst=dst, backup_path=None)
|
|
161
|
+
if dst.exists() and not dst.is_symlink():
|
|
162
|
+
if files_identical(src, dst):
|
|
163
|
+
return InstallAction(action="skip", src=src, dst=dst, backup_path=None)
|
|
164
|
+
backup_path = _timestamped_backup_path(dst)
|
|
165
|
+
return InstallAction(action="overwrite", src=src, dst=dst, backup_path=backup_path)
|
|
166
|
+
return InstallAction(action="install", src=src, dst=dst, backup_path=None)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _plan_component(
|
|
170
|
+
backend: CLIBackend,
|
|
171
|
+
component: str,
|
|
172
|
+
scope: str,
|
|
173
|
+
copy_mode: bool = False,
|
|
174
|
+
) -> List[InstallAction]:
|
|
175
|
+
"""Return InstallActions for one (backend, component, scope) without writing."""
|
|
176
|
+
src = dist_source_for(backend, component)
|
|
177
|
+
if src is None:
|
|
178
|
+
return []
|
|
179
|
+
dst = target_dir_for(backend, component, scope)
|
|
180
|
+
if dst is None:
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
actions: List[InstallAction] = []
|
|
184
|
+
|
|
185
|
+
if component == "config":
|
|
186
|
+
filename = config_filename_for(backend)
|
|
187
|
+
if not filename:
|
|
188
|
+
return []
|
|
189
|
+
src_file = src / filename
|
|
190
|
+
if not src_file.exists():
|
|
191
|
+
return []
|
|
192
|
+
actions.append(_plan_file(src_file, dst / filename, copy_mode))
|
|
193
|
+
elif component in ("agents", "rules", "commands"):
|
|
194
|
+
for src_file in sorted(src.glob("*.md")):
|
|
195
|
+
if src_file.exists():
|
|
196
|
+
actions.append(_plan_file(src_file, dst / src_file.name, copy_mode))
|
|
197
|
+
elif component == "skills":
|
|
198
|
+
if not src.exists():
|
|
199
|
+
return []
|
|
200
|
+
for skill_dir in sorted(d for d in src.iterdir() if d.is_dir()):
|
|
201
|
+
actions.append(_plan_file(skill_dir, dst / skill_dir.name, copy_mode))
|
|
202
|
+
|
|
203
|
+
return actions
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _plan_session_hook(
|
|
207
|
+
backend,
|
|
208
|
+
scope: str,
|
|
209
|
+
) -> List[InstallAction]:
|
|
210
|
+
"""Return InstallActions for the settings.json SessionStart hook write.
|
|
211
|
+
|
|
212
|
+
The hook is injected via merge (never a full overwrite), so:
|
|
213
|
+
- If settings.json does not exist: action="install"
|
|
214
|
+
- If settings.json exists but hook is absent: action="modify"
|
|
215
|
+
- If settings.json exists and hook is already present: action="skip"
|
|
216
|
+
"""
|
|
217
|
+
from .settings_writer import has_hook
|
|
218
|
+
|
|
219
|
+
settings_path, _context_file, hook_command = _session_hook_paths(backend, scope)
|
|
220
|
+
|
|
221
|
+
if not settings_path.exists():
|
|
222
|
+
# Fresh write — settings.json will be created
|
|
223
|
+
return [InstallAction(action="install", src=settings_path, dst=settings_path, backup_path=None)]
|
|
224
|
+
|
|
225
|
+
if has_hook(settings_path, "SessionStart", hook_command):
|
|
226
|
+
# Already installed — no-op
|
|
227
|
+
return [InstallAction(action="skip", src=settings_path, dst=settings_path, backup_path=None)]
|
|
228
|
+
|
|
229
|
+
# Merge inject — file exists but hook is absent; classified as modify
|
|
230
|
+
return [InstallAction(action="modify", src=settings_path, dst=settings_path, backup_path=None)]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def plan_install(
|
|
234
|
+
scope: str,
|
|
235
|
+
registry: Optional[CLIRegistry] = None,
|
|
236
|
+
selected_clis: Optional[set] = None,
|
|
237
|
+
selected_skills: Optional[List[str]] = None,
|
|
238
|
+
copy_mode: bool = False,
|
|
239
|
+
) -> List[InstallAction]:
|
|
240
|
+
"""Return a manifest of what install_all would do, without writing any files.
|
|
241
|
+
|
|
242
|
+
Each entry is an InstallAction(action, src, dst, backup_path) where:
|
|
243
|
+
action == "install" — dst does not yet exist (fresh placement)
|
|
244
|
+
action == "modify" — dst exists and will be merge-updated (e.g. settings.json hook)
|
|
245
|
+
action == "overwrite" — dst exists and differs; backup_path is the timestamped path
|
|
246
|
+
action == "skip" — dst exists and is byte-identical (or symlink unchanged); no write needed
|
|
247
|
+
"""
|
|
248
|
+
if registry is None:
|
|
249
|
+
registry = load_registry()
|
|
250
|
+
|
|
251
|
+
actions: List[InstallAction] = []
|
|
252
|
+
|
|
253
|
+
for backend in registry.all():
|
|
254
|
+
if selected_clis is not None and backend.name not in selected_clis:
|
|
255
|
+
continue
|
|
256
|
+
for component in COMPONENT_TYPES:
|
|
257
|
+
if component == "skills" and selected_skills is not None:
|
|
258
|
+
# Skills are filtered — plan them separately below
|
|
259
|
+
continue
|
|
260
|
+
actions.extend(_plan_component(backend, component, scope, copy_mode))
|
|
261
|
+
|
|
262
|
+
# Skills: respect the selected_skills filter (mirrors wizard's install_skills_filtered)
|
|
263
|
+
if scope == "global" or selected_skills is not None:
|
|
264
|
+
dist_skills_dir = config.DIST_SKILLS_DIR
|
|
265
|
+
if dist_skills_dir.exists():
|
|
266
|
+
skill_dirs = {d.name: d for d in dist_skills_dir.iterdir() if d.is_dir()}
|
|
267
|
+
names_to_plan = selected_skills if selected_skills is not None else list(skill_dirs.keys())
|
|
268
|
+
|
|
269
|
+
# Per-backend skill targets
|
|
270
|
+
for backend in registry.all():
|
|
271
|
+
if selected_clis is not None and backend.name not in selected_clis:
|
|
272
|
+
continue
|
|
273
|
+
if not backend.supports("skills"):
|
|
274
|
+
continue
|
|
275
|
+
dst_dir = target_dir_for(backend, "skills", scope)
|
|
276
|
+
if dst_dir is None:
|
|
277
|
+
continue
|
|
278
|
+
for name in sorted(names_to_plan):
|
|
279
|
+
skill_dir = skill_dirs.get(name)
|
|
280
|
+
if skill_dir:
|
|
281
|
+
actions.append(_plan_file(skill_dir, dst_dir / name, copy_mode))
|
|
282
|
+
|
|
283
|
+
# Universal skills mirror (~/.agents/skills/)
|
|
284
|
+
if scope == "global":
|
|
285
|
+
target = config.AGENTS_HOME / "skills"
|
|
286
|
+
for name in sorted(names_to_plan):
|
|
287
|
+
skill_dir = skill_dirs.get(name)
|
|
288
|
+
if skill_dir:
|
|
289
|
+
actions.append(_plan_file(skill_dir, target / name, copy_mode))
|
|
290
|
+
|
|
291
|
+
# SessionStart hook for Claude Code — always planned when claude backend is selected
|
|
292
|
+
try:
|
|
293
|
+
claude_backend = registry.get("claude")
|
|
294
|
+
if selected_clis is None or claude_backend.name in selected_clis:
|
|
295
|
+
actions.extend(_plan_session_hook(claude_backend, scope))
|
|
296
|
+
except KeyError:
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
return actions
|
|
300
|
+
|
|
301
|
+
|
|
147
302
|
def install_all(scope: str, copy_mode: bool, registry: Optional[CLIRegistry] = None) -> None:
|
|
148
303
|
"""Top-level: install every (backend, component) combo."""
|
|
149
304
|
if registry is None:
|
|
@@ -203,6 +203,7 @@ tests/unit/commands/__init__.py
|
|
|
203
203
|
tests/unit/commands/test_cost_report_subcommand.py
|
|
204
204
|
tests/unit/commands/test_count_agents.py
|
|
205
205
|
tests/unit/commands/test_wizard_orchestrator_skip.py
|
|
206
|
+
tests/unit/commands/test_wizard_preflight.py
|
|
206
207
|
tests/unit/registries/__init__.py
|
|
207
208
|
tests/unit/registries/test_registries.py
|
|
208
209
|
tests/unit/scripts/__init__.py
|
|
@@ -213,6 +214,8 @@ tests/unit/scripts/test_time_aggregation.py
|
|
|
213
214
|
tests/unit/services/__init__.py
|
|
214
215
|
tests/unit/services/test_build_functions.py
|
|
215
216
|
tests/unit/services/test_credentials.py
|
|
217
|
+
tests/unit/services/test_fs.py
|
|
218
|
+
tests/unit/services/test_installer_plan.py
|
|
216
219
|
tests/unit/services/test_memory_backend.py
|
|
217
220
|
tests/unit/services/test_memory_backend_io.py
|
|
218
221
|
tests/unit/services/test_rendering_includes.py
|
|
@@ -8,13 +8,8 @@ from agent_notes.config import DIST_DIR
|
|
|
8
8
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"""Build dist once at suite start. If build fails, the entire suite errors loudly — no silent skips.
|
|
14
|
-
|
|
15
|
-
Tests that need a built dist depend on this implicitly (autouse). Tests that don't
|
|
16
|
-
consume it pay only the one-time build cost.
|
|
17
|
-
"""
|
|
11
|
+
def pytest_sessionstart(session):
|
|
12
|
+
"""Build dist before collection so module-level discovery in test files works."""
|
|
18
13
|
result = subprocess.run(
|
|
19
14
|
["python3", "-m", "agent_notes", "build"],
|
|
20
15
|
cwd=REPO_ROOT,
|
|
@@ -27,4 +22,13 @@ def built_dist():
|
|
|
27
22
|
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}",
|
|
28
23
|
returncode=1,
|
|
29
24
|
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
28
|
+
def built_dist():
|
|
29
|
+
"""Build dist once at suite start. If build fails, the entire suite errors loudly — no silent skips.
|
|
30
|
+
|
|
31
|
+
Tests that need a built dist depend on this implicitly (autouse). Tests that don't
|
|
32
|
+
consume it pay only the one-time build cost.
|
|
33
|
+
"""
|
|
30
34
|
yield DIST_DIR
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Tests for _confirm_install pre-flight summary in agent_notes.commands.wizard."""
|
|
2
|
+
import io
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import pytest
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import patch, MagicMock
|
|
8
|
+
|
|
9
|
+
from agent_notes.services.installer import InstallAction
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# Helpers
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
def _make_install_action(action: str, dst_name: str = "CLAUDE.md", backup: Path = None) -> InstallAction:
|
|
17
|
+
src = Path("/fake/src") / dst_name
|
|
18
|
+
dst = Path("/fake/dst") / dst_name
|
|
19
|
+
return InstallAction(action=action, src=src, dst=dst, backup_path=backup)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _run_confirm_install(monkeypatch, manifest, user_input: str = "Y"):
|
|
23
|
+
"""Call _confirm_install with all heavy side-effects mocked out.
|
|
24
|
+
|
|
25
|
+
local imports inside _confirm_install:
|
|
26
|
+
from ..services.ui import _clear_screen, _render_step_header
|
|
27
|
+
from ..registries.cli_registry import load_registry
|
|
28
|
+
from ..services.installer import plan_install
|
|
29
|
+
|
|
30
|
+
We patch the source modules so the local imports pick up the stubs.
|
|
31
|
+
"""
|
|
32
|
+
# Suppress screen-clearing and step headers (patched at source)
|
|
33
|
+
monkeypatch.setattr("agent_notes.services.ui._clear_screen", lambda: None)
|
|
34
|
+
monkeypatch.setattr("agent_notes.services.ui._render_step_header", lambda *a, **kw: None)
|
|
35
|
+
|
|
36
|
+
# plan_install is imported from services.installer inside the function
|
|
37
|
+
monkeypatch.setattr("agent_notes.services.installer.plan_install", lambda **kw: manifest)
|
|
38
|
+
|
|
39
|
+
# load_registry is imported from registries.cli_registry inside the function
|
|
40
|
+
monkeypatch.setattr(
|
|
41
|
+
"agent_notes.registries.cli_registry.load_registry",
|
|
42
|
+
lambda *a, **kw: MagicMock(),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Stub out module-level helpers in wizard
|
|
46
|
+
monkeypatch.setattr("agent_notes.commands.wizard._render_install_summary", lambda *a, **kw: None)
|
|
47
|
+
monkeypatch.setattr("agent_notes.commands.wizard._get_skill_groups", lambda: {})
|
|
48
|
+
|
|
49
|
+
# Drive _safe_input (module-level import in wizard)
|
|
50
|
+
monkeypatch.setattr("agent_notes.commands.wizard._safe_input", lambda prompt, default: user_input)
|
|
51
|
+
|
|
52
|
+
# Redirect stdout to capture prints
|
|
53
|
+
buf = io.StringIO()
|
|
54
|
+
with patch("sys.stdout", buf):
|
|
55
|
+
from agent_notes.commands.wizard import _confirm_install
|
|
56
|
+
result = _confirm_install(
|
|
57
|
+
clis={"claude"},
|
|
58
|
+
scope="local",
|
|
59
|
+
copy_mode=False,
|
|
60
|
+
selected_skills=[],
|
|
61
|
+
role_models={},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return result, buf.getvalue()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Tests
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
class TestConfirmInstallFileCount:
|
|
72
|
+
def test_shows_files_to_install_count(self, monkeypatch):
|
|
73
|
+
"""The 'Files to install: N' line must show the correct non-skip count."""
|
|
74
|
+
manifest = [
|
|
75
|
+
_make_install_action("install", "a.md"),
|
|
76
|
+
_make_install_action("install", "b.md"),
|
|
77
|
+
_make_install_action("install", "c.md"),
|
|
78
|
+
_make_install_action("skip", "d.md"), # not counted
|
|
79
|
+
]
|
|
80
|
+
_result, output = _run_confirm_install(monkeypatch, manifest, user_input="Y")
|
|
81
|
+
|
|
82
|
+
assert "Files to install:" in output
|
|
83
|
+
# The count of non-skip actions is 3
|
|
84
|
+
assert "3" in output
|
|
85
|
+
|
|
86
|
+
def test_skip_actions_excluded_from_count(self, monkeypatch):
|
|
87
|
+
"""All-skip manifest reports 0 files to install."""
|
|
88
|
+
manifest = [_make_install_action("skip", f"{i}.md") for i in range(5)]
|
|
89
|
+
_result, output = _run_confirm_install(monkeypatch, manifest, user_input="Y")
|
|
90
|
+
|
|
91
|
+
assert "Files to install:" in output
|
|
92
|
+
assert "0" in output
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TestConfirmInstallBackupLines:
|
|
96
|
+
def test_lists_each_backup_path_when_overwrites_present(self, monkeypatch, tmp_path):
|
|
97
|
+
"""Each overwrite action with a backup_path must appear in its own output line."""
|
|
98
|
+
bak1 = tmp_path / "CLAUDE.md.bak.20260430T022500000001Z"
|
|
99
|
+
bak2 = tmp_path / "settings.json.bak.20260430T022500000002Z"
|
|
100
|
+
manifest = [
|
|
101
|
+
_make_install_action("install", "agents/role.md"),
|
|
102
|
+
InstallAction(
|
|
103
|
+
action="overwrite",
|
|
104
|
+
src=Path("/fake/src/CLAUDE.md"),
|
|
105
|
+
dst=tmp_path / "CLAUDE.md",
|
|
106
|
+
backup_path=bak1,
|
|
107
|
+
),
|
|
108
|
+
InstallAction(
|
|
109
|
+
action="overwrite",
|
|
110
|
+
src=Path("/fake/src/settings.json"),
|
|
111
|
+
dst=tmp_path / "settings.json",
|
|
112
|
+
backup_path=bak2,
|
|
113
|
+
),
|
|
114
|
+
]
|
|
115
|
+
_result, output = _run_confirm_install(monkeypatch, manifest, user_input="Y")
|
|
116
|
+
|
|
117
|
+
assert str(bak1) in output, f"Expected backup path {bak1} in output:\n{output}"
|
|
118
|
+
assert str(bak2) in output, f"Expected backup path {bak2} in output:\n{output}"
|
|
119
|
+
|
|
120
|
+
def test_backup_section_header_present_when_overwrites_exist(self, monkeypatch, tmp_path):
|
|
121
|
+
bak = tmp_path / "CLAUDE.md.bak.20260430T022500000001Z"
|
|
122
|
+
manifest = [
|
|
123
|
+
InstallAction(
|
|
124
|
+
action="overwrite",
|
|
125
|
+
src=Path("/fake/src/CLAUDE.md"),
|
|
126
|
+
dst=tmp_path / "CLAUDE.md",
|
|
127
|
+
backup_path=bak,
|
|
128
|
+
),
|
|
129
|
+
]
|
|
130
|
+
_result, output = _run_confirm_install(monkeypatch, manifest, user_input="Y")
|
|
131
|
+
|
|
132
|
+
assert "back up" in output.lower() or "backup" in output.lower(), (
|
|
133
|
+
f"Expected a backup section header in output:\n{output}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def test_no_backup_section_when_no_overwrites(self, monkeypatch):
|
|
137
|
+
"""If no overwrites, the backup listing block must not appear."""
|
|
138
|
+
manifest = [
|
|
139
|
+
_make_install_action("install", "a.md"),
|
|
140
|
+
_make_install_action("skip", "b.md"),
|
|
141
|
+
]
|
|
142
|
+
_result, output = _run_confirm_install(monkeypatch, manifest, user_input="Y")
|
|
143
|
+
|
|
144
|
+
# "back up" section only appears when there are overwrites
|
|
145
|
+
assert "→" not in output, (
|
|
146
|
+
"No backup arrow (→) should appear when there are no overwrites"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TestConfirmInstallPlanInstallException:
|
|
151
|
+
def test_plan_install_exception_does_not_raise_to_user(self, monkeypatch):
|
|
152
|
+
"""When plan_install raises, _confirm_install must not propagate the exception."""
|
|
153
|
+
monkeypatch.setattr("agent_notes.services.ui._clear_screen", lambda: None)
|
|
154
|
+
monkeypatch.setattr("agent_notes.services.ui._render_step_header", lambda *a, **kw: None)
|
|
155
|
+
monkeypatch.setattr("agent_notes.commands.wizard._render_install_summary", lambda *a, **kw: None)
|
|
156
|
+
monkeypatch.setattr("agent_notes.commands.wizard._get_skill_groups", lambda: {})
|
|
157
|
+
monkeypatch.setattr(
|
|
158
|
+
"agent_notes.registries.cli_registry.load_registry",
|
|
159
|
+
lambda *a, **kw: MagicMock(),
|
|
160
|
+
)
|
|
161
|
+
monkeypatch.setattr("agent_notes.commands.wizard._safe_input", lambda prompt, default: "Y")
|
|
162
|
+
|
|
163
|
+
def exploding_plan_install(**kw):
|
|
164
|
+
raise RuntimeError("simulated plan_install failure")
|
|
165
|
+
|
|
166
|
+
monkeypatch.setattr("agent_notes.services.installer.plan_install", exploding_plan_install)
|
|
167
|
+
|
|
168
|
+
# Should not raise
|
|
169
|
+
from agent_notes.commands.wizard import _confirm_install
|
|
170
|
+
result = _confirm_install(
|
|
171
|
+
clis={"claude"},
|
|
172
|
+
scope="local",
|
|
173
|
+
copy_mode=False,
|
|
174
|
+
selected_skills=[],
|
|
175
|
+
role_models={},
|
|
176
|
+
)
|
|
177
|
+
# User answered Y, so result should be True (proceed)
|
|
178
|
+
assert result is True
|
|
179
|
+
|
|
180
|
+
def test_plan_install_exception_emits_debug_log(self, monkeypatch, caplog):
|
|
181
|
+
"""When plan_install raises, a debug-level log must be emitted."""
|
|
182
|
+
monkeypatch.setattr("agent_notes.services.ui._clear_screen", lambda: None)
|
|
183
|
+
monkeypatch.setattr("agent_notes.services.ui._render_step_header", lambda *a, **kw: None)
|
|
184
|
+
monkeypatch.setattr("agent_notes.commands.wizard._render_install_summary", lambda *a, **kw: None)
|
|
185
|
+
monkeypatch.setattr("agent_notes.commands.wizard._get_skill_groups", lambda: {})
|
|
186
|
+
monkeypatch.setattr(
|
|
187
|
+
"agent_notes.registries.cli_registry.load_registry",
|
|
188
|
+
lambda *a, **kw: MagicMock(),
|
|
189
|
+
)
|
|
190
|
+
monkeypatch.setattr("agent_notes.commands.wizard._safe_input", lambda prompt, default: "Y")
|
|
191
|
+
|
|
192
|
+
def exploding_plan_install(**kw):
|
|
193
|
+
raise ValueError("boom")
|
|
194
|
+
|
|
195
|
+
monkeypatch.setattr("agent_notes.services.installer.plan_install", exploding_plan_install)
|
|
196
|
+
|
|
197
|
+
with caplog.at_level(logging.DEBUG, logger="agent_notes.commands.wizard"):
|
|
198
|
+
from agent_notes.commands.wizard import _confirm_install
|
|
199
|
+
_confirm_install(
|
|
200
|
+
clis={"claude"},
|
|
201
|
+
scope="local",
|
|
202
|
+
copy_mode=False,
|
|
203
|
+
selected_skills=[],
|
|
204
|
+
role_models={},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
assert any(
|
|
208
|
+
"plan_install" in r.message.lower() or "pre-flight" in r.message.lower()
|
|
209
|
+
for r in caplog.records
|
|
210
|
+
), f"Expected a debug log mentioning plan_install/pre-flight, got: {caplog.records}"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class TestConfirmInstallAbort:
|
|
214
|
+
def test_returns_false_when_user_answers_n(self, monkeypatch):
|
|
215
|
+
"""When user answers 'n', _confirm_install must return False."""
|
|
216
|
+
manifest = [_make_install_action("install", "a.md")]
|
|
217
|
+
result, _output = _run_confirm_install(monkeypatch, manifest, user_input="n")
|
|
218
|
+
assert result is False
|
|
219
|
+
|
|
220
|
+
def test_returns_true_when_user_answers_y(self, monkeypatch):
|
|
221
|
+
manifest = [_make_install_action("install", "a.md")]
|
|
222
|
+
result, _output = _run_confirm_install(monkeypatch, manifest, user_input="Y")
|
|
223
|
+
assert result is True
|
|
224
|
+
|
|
225
|
+
def test_no_install_called_when_user_aborts(self, monkeypatch):
|
|
226
|
+
"""Confirming 'n' must not trigger any install side-effects.
|
|
227
|
+
_confirm_install only returns a bool — actual install is the caller's job.
|
|
228
|
+
We verify the return value is False (caller must check it).
|
|
229
|
+
"""
|
|
230
|
+
manifest = [_make_install_action("install", "a.md")]
|
|
231
|
+
result, _output = _run_confirm_install(monkeypatch, manifest, user_input="n")
|
|
232
|
+
assert result is False, "Return value False signals caller should not proceed"
|