Install skills with optional legacy migration.
(
self,
project_root: Path,
manifest: IntegrationManifest,
parsed_options: dict[str, Any] | None = None,
**opts: Any,
)
| 83 | ] |
| 84 | |
| 85 | def setup( |
| 86 | self, |
| 87 | project_root: Path, |
| 88 | manifest: IntegrationManifest, |
| 89 | parsed_options: dict[str, Any] | None = None, |
| 90 | **opts: Any, |
| 91 | ) -> list[Path]: |
| 92 | """Install skills with optional legacy migration.""" |
| 93 | parsed_options = parsed_options or {} |
| 94 | |
| 95 | # Refuse a symlinked destination before any writes occur. base |
| 96 | # setup() only rejects a destination that *escapes* project_root |
| 97 | # after resolve(), so an in-tree symlinked ``.kimi-code`` / |
| 98 | # ``.kimi-code/skills`` (e.g. ``-> .``) would still pass that check |
| 99 | # and misdirect the SKILL.md writes into an unintended in-tree |
| 100 | # location (e.g. ``./skills/``). Reject any symlinked destination |
| 101 | # component up front so this never happens. |
| 102 | new_skills_dir = self.skills_dest(project_root) |
| 103 | if _has_symlinked_component(new_skills_dir, project_root): |
| 104 | raise ValueError( |
| 105 | f"Skills destination {new_skills_dir} contains a symlinked " |
| 106 | f"path component; refusing to install into it." |
| 107 | ) |
| 108 | |
| 109 | # Run base setup first so new-path targets (speckit-*) exist, |
| 110 | # then migrate/clean legacy dirs without risking user content loss. |
| 111 | created = super().setup( |
| 112 | project_root, manifest, parsed_options=parsed_options, **opts |
| 113 | ) |
| 114 | |
| 115 | if parsed_options.get("migrate_legacy", False): |
| 116 | old_skills_dir = project_root / ".kimi" / "skills" |
| 117 | # Validate both endpoints. base setup() already rejects a |
| 118 | # destination that *escapes* the project root, but an in-tree |
| 119 | # symlinked ``.kimi-code``/``.kimi-code/skills`` (e.g. ``-> .``) |
| 120 | # would still misdirect the move; ``_is_safe_legacy_dir`` rejects |
| 121 | # any symlinked component, giving the destination the same |
| 122 | # protection as the source. |
| 123 | if _is_safe_legacy_dir(old_skills_dir, project_root) and ( |
| 124 | _is_safe_legacy_dir(new_skills_dir, project_root) |
| 125 | ): |
| 126 | _migrate_legacy_kimi_skills_dir(old_skills_dir, new_skills_dir) |
| 127 | |
| 128 | return created |
| 129 | |
| 130 | def teardown( |
| 131 | self, |
nothing calls this directly
no test coverage detected