Skip to content

Commit f65d9f9

Browse files
PascalThuetclaudeCopilot
authored
feat(integration): add status reporting (#2674)
* feat(integration): add status reporting * docs(integration): include status in query command docstring * fix(integration): handle Windows extended-length paths in status containment On Windows, os.readlink() (and sometimes Path.resolve()) return paths with the \\?\ extended-length prefix. Comparing such a target against a plain project root via Path.relative_to() spuriously fails, so an in-project dangling symlink was classified as `invalid` instead of `missing` — failing test_status_treats_dangling_symlink_as_missing and the windows-style variant on the Windows CI runners. Centralize the containment check in _is_within_project() and strip the \\?\ / \\?\UNC\ prefix from both sides before relative_to(). Add portable regression tests for the prefix-stripping helper and the containment contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * test(integration): restore top-level pytest import after rebase A three-way merge / rebase onto main silently dropped the module-level `import pytest` from test_integration_subcommand.py: main reorganized the import block without it (using only a local `import pytest as _pytest`), while this branch added top-level fixtures and `pytest.skip`/`pytest.raises` usage. The overlapping import-hunk edits resolved by dropping the import, breaking collection with `NameError: name 'pytest' is not defined` on every runner. Re-add the import in the third-party group. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(integration): fix Windows UNC path assertion in status helper test `test_strip_extended_length_prefix_normalizes_windows_paths` compared the str() form of the helper's output against a hand-built string. On Windows, pathlib renders a UNC root with a trailing separator (`\\server\share\`), so the exact string match failed there (`\\server\share\` != `\\server\share`) even though `_strip_extended_length_prefix` behaves correctly — the trailing separator is irrelevant to the `relative_to` containment check it feeds. Compare Path objects (semantic equality) instead of exact strings so the assertion holds on both POSIX and Windows. No production code change needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(integration): make shared-manifest remediation specify --integration The fallback `_manifest_suggestion` for the shared `speckit` manifest (used when no usable default integration is recorded) suggested `specify init --here --force`, which can trigger interactive integration selection. For CI/agent consumers of `integration status`, surface an explicit `--integration <key>` placeholder, matching the file's existing `<key>` suggestion style. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent ad9f047 commit f65d9f9

6 files changed

Lines changed: 1673 additions & 14 deletions

File tree

docs/reference/integrations.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,27 @@ specify integration upgrade [<key>]
126126

127127
Reinstalls an installed integration with updated templates and commands (e.g., after upgrading Spec Kit). Defaults to the default integration; if a key is provided, it must be one of the installed integrations. Detects locally modified files and blocks the upgrade unless `--force` is used. Stale files from the previous install that are no longer needed are removed automatically. Shared templates stay aligned with the default integration even when upgrading a non-default integration.
128128

129+
## Report Integration Status
130+
131+
```bash
132+
specify integration status
133+
specify integration status --json
134+
```
135+
136+
Reports the current project's integration status without changing files. The
137+
status report includes the default integration, installed integrations,
138+
multi-install safety, missing managed files, modified managed files, invalid
139+
manifest paths, shared Spec Kit infrastructure health, unchecked manifests, and
140+
the target integration for default-sensitive shared templates. The JSON form is
141+
intended for CI and coding agents that need stable machine-readable status data;
142+
it also reports the raw recorded integrations and the integration manifests that
143+
were checked when state repair heuristics differ from the recorded file.
144+
The command exits 0 when the report status is `ok` or `warning`; it exits 1
145+
only when the report status is `error`. In JSON output, `multi_install_safe`
146+
is `null` when no installed integration set can be evaluated, such as when the
147+
integration state is missing, unreadable, lacks a valid recorded integration
148+
list, or records no installed integrations.
149+
129150
## Integration-Specific Options
130151

131152
Some integrations accept additional options via `--integration-options`:

src/specify_cli/integration_state.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,14 @@ class IntegrationReadError:
2525
schema: int | None = None
2626

2727

28-
def try_read_integration_json(
28+
def _read_integration_json_data(
2929
project_root: Path,
3030
) -> tuple[dict[str, Any] | None, IntegrationReadError | None]:
31-
"""Parse ``.specify/integration.json`` without raising.
31+
"""Read raw integration state without normalizing or raising.
3232
33-
Returns ``(normalized_state, None)`` on success, ``(None, None)`` when the
34-
file does not exist, or ``(None, error)`` for any parse / validation
35-
failure. This is the single low-level reader; both the CLI's loud
36-
``_read_integration_json`` and the workflow engine's silent
37-
``_load_project_integration`` consume it so the schema guard and parse
38-
logic cannot drift between them.
33+
Returns ``(data, None)`` when the JSON object is readable and supported,
34+
``(None, None)`` when the file is absent, and ``(None, error)`` for parse,
35+
schema, encoding, or filesystem failures.
3936
"""
4037
path = project_root / INTEGRATION_JSON
4138
# Avoid Path.exists() / Path.is_file() as a pre-check: both return False
@@ -70,9 +67,41 @@ def try_read_integration_json(
7067
and schema > INTEGRATION_STATE_SCHEMA
7168
):
7269
return None, IntegrationReadError(kind="schema_too_new", schema=schema)
70+
return data, None
71+
72+
73+
def try_read_integration_json(
74+
project_root: Path,
75+
) -> tuple[dict[str, Any] | None, IntegrationReadError | None]:
76+
"""Parse ``.specify/integration.json`` without raising.
77+
78+
Returns ``(normalized_state, None)`` on success, ``(None, None)`` when the
79+
file does not exist, or ``(None, error)`` for any parse / validation
80+
failure. This helper delegates file I/O and raw JSON validation to
81+
``_read_integration_json_data`` so callers that need raw state can share
82+
the same low-level reader instead of duplicating parse logic.
83+
"""
84+
data, error = _read_integration_json_data(project_root)
85+
if data is None:
86+
return None, error
7387
return normalize_integration_state(data), None
7488

7589

90+
def try_read_integration_json_with_raw(
91+
project_root: Path,
92+
) -> tuple[dict[str, Any] | None, dict[str, Any] | None, IntegrationReadError | None]:
93+
"""Parse ``integration.json`` and return normalized plus raw state.
94+
95+
Returns ``(normalized_state, raw_state, None)`` when the file is readable,
96+
``(None, None, None)`` when it is absent, and ``(None, None, error)`` for
97+
parse, schema, encoding, or filesystem failures.
98+
"""
99+
data, error = _read_integration_json_data(project_root)
100+
if data is None:
101+
return None, None, error
102+
return normalize_integration_state(data), data, None
103+
104+
76105
def clean_integration_key(key: Any) -> str | None:
77106
"""Return a stripped integration key, or None for empty/non-string values."""
78107
if not isinstance(key, str) or not key.strip():

0 commit comments

Comments
 (0)