* feat: Stage 1 — integration foundation (base classes, manifest, registry)
Add the integrations package with:
- IntegrationBase ABC and MarkdownIntegration base class
- IntegrationOption dataclass for per-integration CLI options
- IntegrationManifest with SHA-256 hash-tracked install/uninstall
- INTEGRATION_REGISTRY (empty, populated in later stages)
- 34 tests at 98% coverage
Purely additive — no existing code modified.
Part of #1924
* fix: normalize manifest keys to POSIX, type manifest parameter
- Store manifest file keys using as_posix() after resolving relative
to project root, ensuring cross-platform portable manifests
- Type the manifest parameter as IntegrationManifest (via TYPE_CHECKING
import) instead of Any in IntegrationBase methods
* fix: symlink safety in uninstall/setup, handle invalid JSON in load
- uninstall() now uses non-resolved path for deletion so symlinks
themselves are removed, not their targets; resolve only for
containment validation
- setup() keeps unresolved dst_file for copy; resolves separately
for project-root validation
- load() catches json.JSONDecodeError and re-raises as ValueError
with the manifest path for clearer diagnostics
- Added test for invalid JSON manifest loading
* fix: lexical symlink containment, assert project_root consistency
- uninstall() now uses os.path.normpath for lexical containment check
instead of resolve(), so in-project symlinks pointing outside are
still properly removed
- setup() asserts manifest.project_root matches the passed project_root
to prevent path mismatches between file operations and manifest
recording
* fix: handle non-files in check_modified/uninstall, validate manifest key
- check_modified() treats non-regular-files (dirs, symlinks) as modified
instead of crashing with IsADirectoryError
- uninstall() skips directories (adds to skipped list), only unlinks
files and symlinks
- load() validates stored integration key matches the requested key
* fix: safe symlink handling in uninstall
- Broken symlinks now removable (lexists check via is_symlink fallback)
- Symlinks never hashed (avoids following to external targets)
- Symlinks only removed with force=True, otherwise skipped
* fix: robust unlink, fail-fast config validation, symlink tests
- uninstall() wraps path.unlink() in try/except OSError to avoid
partial cleanup on race conditions or permission errors
- setup() raises ValueError on missing config or folder instead of
silently returning empty
- Added 3 tests: symlink in check_modified, symlink skip/force in
uninstall (47 total)
* fix: check_modified uses lexical containment, explicit is_symlink check
- check_modified() no longer calls _validate_rel_path (which resolves
symlinks); uses lexical checks (is_absolute, '..' in parts) instead
- is_symlink() checked before is_file() so symlinks to files are still
treated as modified
- Fixed templates_dir() docstring to match actual behavior
---------
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>