mirror of
https://github.com/github/spec-kit.git
synced 2026-07-05 05:21:48 +08:00
* support controlled multi-install integrations * fix: harden multi-install integration state * refactor: isolate integration runtime helpers * fix: address copilot review feedback * fix: address follow-up copilot feedback * fix: tighten integration switch semantics * fix: address final copilot review feedback * fix: harden integration manifest read errors * fix: refuse symlinked shared infra paths * test: filter expected self-test preset warning * test: address copilot review nits * refactor: centralize safe shared infra writes * fix: use no-follow writes for shared infra * fix: keep default integration atomic on template refresh * fix: harden shared infra error paths * fix: preflight shared infra and future state schemas * fix: support nested shared scripts during preflight * test: tolerate wrapped schema error output * fix: use safe default mode for shared text writes * fix: use posix paths in shared skip output * fix: share project guard for integration use * fix: centralize spec-kit project guards * fix: use posix project paths in cli output * fix: harden shared manifest and upgrade refresh
162 lines
5.6 KiB
Python
162 lines
5.6 KiB
Python
"""State helpers for installed AI agent integrations."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
INTEGRATION_JSON = ".specify/integration.json"
|
|
INTEGRATION_STATE_SCHEMA = 1
|
|
|
|
|
|
def clean_integration_key(key: Any) -> str | None:
|
|
"""Return a stripped integration key, or None for empty/non-string values."""
|
|
if not isinstance(key, str) or not key.strip():
|
|
return None
|
|
return key.strip()
|
|
|
|
|
|
def dedupe_integration_keys(keys: list[Any]) -> list[str]:
|
|
"""Return a de-duplicated list of non-empty integration keys."""
|
|
seen: set[str] = set()
|
|
deduped: list[str] = []
|
|
for key in keys:
|
|
clean = clean_integration_key(key)
|
|
if clean is None:
|
|
continue
|
|
if clean in seen:
|
|
continue
|
|
seen.add(clean)
|
|
deduped.append(clean)
|
|
return deduped
|
|
|
|
|
|
def normalize_integration_settings(settings: Any) -> dict[str, dict[str, Any]]:
|
|
"""Return JSON-safe per-integration runtime settings."""
|
|
if not isinstance(settings, dict):
|
|
return {}
|
|
|
|
normalized: dict[str, dict[str, Any]] = {}
|
|
for key, value in settings.items():
|
|
if not isinstance(key, str) or not key.strip() or not isinstance(value, dict):
|
|
continue
|
|
|
|
clean: dict[str, Any] = {}
|
|
script = value.get("script")
|
|
if isinstance(script, str) and script.strip():
|
|
clean["script"] = script.strip()
|
|
|
|
raw_options = value.get("raw_options")
|
|
if isinstance(raw_options, str):
|
|
clean["raw_options"] = raw_options
|
|
|
|
parsed_options = value.get("parsed_options")
|
|
if isinstance(parsed_options, dict):
|
|
clean["parsed_options"] = parsed_options
|
|
|
|
invoke_separator = value.get("invoke_separator")
|
|
if isinstance(invoke_separator, str) and invoke_separator.strip():
|
|
clean["invoke_separator"] = invoke_separator.strip()
|
|
|
|
if clean:
|
|
normalized[key.strip()] = clean
|
|
|
|
return normalized
|
|
|
|
|
|
def _normalized_integration_state_schema(value: Any) -> int:
|
|
if isinstance(value, int) and not isinstance(value, bool) and value > INTEGRATION_STATE_SCHEMA:
|
|
return value
|
|
return INTEGRATION_STATE_SCHEMA
|
|
|
|
|
|
def normalize_integration_state(data: dict[str, Any]) -> dict[str, Any]:
|
|
"""Normalize legacy and multi-install integration metadata."""
|
|
legacy_key = clean_integration_key(data.get("integration"))
|
|
default_key = clean_integration_key(data.get("default_integration")) or legacy_key
|
|
|
|
installed = data.get("installed_integrations")
|
|
installed_keys = dedupe_integration_keys(installed if isinstance(installed, list) else [])
|
|
if not default_key and installed_keys:
|
|
default_key = installed_keys[0]
|
|
if default_key and default_key not in installed_keys:
|
|
installed_keys.insert(0, default_key)
|
|
|
|
settings = normalize_integration_settings(data.get("integration_settings"))
|
|
|
|
normalized = dict(data)
|
|
normalized["integration_state_schema"] = _normalized_integration_state_schema(
|
|
data.get("integration_state_schema")
|
|
)
|
|
if default_key:
|
|
normalized["integration"] = default_key
|
|
normalized["default_integration"] = default_key
|
|
else:
|
|
normalized.pop("integration", None)
|
|
normalized.pop("default_integration", None)
|
|
normalized["installed_integrations"] = installed_keys
|
|
normalized["integration_settings"] = {
|
|
key: settings[key] for key in installed_keys if key in settings
|
|
}
|
|
return normalized
|
|
|
|
|
|
def default_integration_key(state: dict[str, Any]) -> str | None:
|
|
"""Return the default integration key from normalized state."""
|
|
key = state.get("default_integration") or state.get("integration")
|
|
return clean_integration_key(key)
|
|
|
|
|
|
def installed_integration_keys(state: dict[str, Any]) -> list[str]:
|
|
"""Return installed integration keys from normalized state."""
|
|
return dedupe_integration_keys(state.get("installed_integrations", []))
|
|
|
|
|
|
def integration_settings(state: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
"""Return normalized per-integration settings from state."""
|
|
return normalize_integration_settings(state.get("integration_settings"))
|
|
|
|
|
|
def integration_setting(state: dict[str, Any], key: str) -> dict[str, Any]:
|
|
"""Return stored runtime settings for *key*."""
|
|
return dict(integration_settings(state).get(key, {}))
|
|
|
|
|
|
def write_integration_json(
|
|
project_root: Path,
|
|
*,
|
|
version: str,
|
|
integration_key: str | None,
|
|
installed_integrations: list[str] | None = None,
|
|
settings: dict[str, dict[str, Any]] | None = None,
|
|
) -> None:
|
|
"""Write ``.specify/integration.json`` with legacy-compatible state."""
|
|
dest = project_root / INTEGRATION_JSON
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
integration_key = clean_integration_key(integration_key)
|
|
installed = dedupe_integration_keys(installed_integrations or [])
|
|
if integration_key and integration_key not in installed:
|
|
installed.insert(0, integration_key)
|
|
if not integration_key and installed:
|
|
integration_key = installed[0]
|
|
|
|
normalized_settings = normalize_integration_settings(settings or {})
|
|
normalized_settings = {
|
|
key: normalized_settings[key] for key in installed if key in normalized_settings
|
|
}
|
|
|
|
data: dict[str, Any] = {
|
|
"version": version,
|
|
"integration_state_schema": INTEGRATION_STATE_SCHEMA,
|
|
"installed_integrations": installed,
|
|
"integration_settings": normalized_settings,
|
|
}
|
|
if integration_key:
|
|
data["integration"] = integration_key
|
|
data["default_integration"] = integration_key
|
|
|
|
dest.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|