Skip to content

Commit 6074227

Browse files
committed
fix: give each info entry its provenance as source; empty descriptions as ""
Each command, template and script entry's source is now the layer and the pack that provide it, as in #4213: {"layer": "preset", "presetId": ...} or {"layer": "extension", "extensionId": ...}. The top-level source keeps where the pack was installed from, as in list --json. A preset contribution with `description:` and no value came out as null: the preset manifest checks only type, name and file per template. Anything but a string is now "", with a test. Both reference pages are updated. Assisted-by: Claude Code (model: Claude Opus 5.5, autonomous)
1 parent 7e9aed2 commit 6074227

4 files changed

Lines changed: 45 additions & 15 deletions

File tree

‎docs/reference/extensions.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@ unique display name, ignoring case. The object has the same `id`, `name`,
139139
the matching `specify extension list --json` item. In place of the `provides`
140140
counts it has `commands`, `templates`, `scripts`, and `hooks` arrays. Command,
141141
template, and script entries have `name`, `description` (`""` when absent),
142-
`source` (the extension's `source`), and `sourcePath` (the manifest `file`,
142+
`source` (`{"layer": "extension", "extensionId": "<id>"}`, the extension that
143+
provides it; the top-level `source` is where it was installed from), and
144+
`sourcePath` (the manifest `file`,
143145
relative to the extension directory); script entries also have `runtimes` when
144146
the manifest declares them. Extension entries carry no `strategy`, because
145147
extension-provided files always replace. Each hook entry has `trigger` (the

‎docs/reference/presets.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ JSON object to stdout. The object has the same `id`, `name`, `description`,
135135
`specify preset list --json` item. In place of the `provides` counts it has
136136
`commands`, `templates`, and `scripts` arrays, one entry per manifest
137137
contribution in declaration order. Each entry has `name`, `description` (`""`
138-
when absent), `source` (the preset's `source`), `sourcePath` (the manifest
138+
when absent or empty), `source` (`{"layer": "preset", "presetId": "<id>"}`, the
139+
preset that provides it; the top-level `source` is where it was installed
140+
from), `sourcePath` (the manifest
139141
`file`, relative to the preset directory), and `strategy` (`replace`,
140142
`prepend`, `append`, or `wrap`; `replace` when the manifest omits it). `--json`
141143
cannot be combined with `--versions` (usage error, exit 2). A preset that is

‎src/specify_cli/_installed_info_json.py‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,17 @@ def _find_installed(
3737

3838

3939
def _contribution(entry: dict[str, Any], source: dict[str, str]) -> dict[str, Any]:
40-
"""Return one command, template or script entry of an installed pack."""
40+
"""Return one command, template or script entry of an installed pack.
41+
42+
``source`` is the entry's provenance, the layer and the pack that provide
43+
it (#4213); where the pack was installed from is the top-level ``source``.
44+
"""
45+
description = entry.get("description")
4146
return {
4247
"name": entry["name"],
43-
"description": entry.get("description", ""),
48+
# ``description:`` with no value reads as None, and the preset manifest
49+
# does not check contribution descriptions
50+
"description": description if isinstance(description, str) else "",
4451
"source": dict(source),
4552
"sourcePath": entry["file"],
4653
}
@@ -56,8 +63,9 @@ def preset_info_item(records: list[dict[str, Any]], manager: Any, key: str) -> d
5663
item = installed_list_item(record, include_hooks=False)
5764
del item["provides"]
5865
groups: dict[str, list[dict[str, Any]]] = {"commands": [], "templates": [], "scripts": []}
66+
provenance = {"layer": "preset", "presetId": record["id"]}
5967
for template in manifest.templates:
60-
entry = _contribution(template, item["source"])
68+
entry = _contribution(template, provenance)
6169
entry["strategy"] = template.get("strategy", "replace")
6270
groups[f"{template['type']}s"].append(entry)
6371
return {**item, **groups}
@@ -74,9 +82,10 @@ def extension_info_item(records: list[dict[str, Any]], manager: Any, key: str) -
7482

7583
item = installed_list_item(record, include_hooks=True)
7684
del item["provides"]
85+
provenance = {"layer": "extension", "extensionId": record["id"]}
7786
scripts = []
7887
for script in manifest.scripts:
79-
entry = _contribution(script, item["source"])
88+
entry = _contribution(script, provenance)
8089
if "runtimes" in script:
8190
entry["runtimes"] = list(script["runtimes"])
8291
scripts.append(entry)
@@ -103,8 +112,8 @@ def extension_info_item(records: list[dict[str, Any]], manager: Any, key: str) -
103112

104113
return {
105114
**item,
106-
"commands": [_contribution(command, item["source"]) for command in manifest.commands],
107-
"templates": [_contribution(template, item["source"]) for template in manifest.templates],
115+
"commands": [_contribution(command, provenance) for command in manifest.commands],
116+
"templates": [_contribution(template, provenance) for template in manifest.templates],
108117
"scripts": scripts,
109118
"hooks": hooks,
110119
}

‎tests/test_installed_info_json.py‎

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
runner = CliRunner()
1616

1717
SOURCE = {"kind": "catalog", "catalog": "speckit-official"}
18+
# provenance of each contribution, as in #4213; SOURCE is where the pack was installed from
19+
PRESET_SOURCE = {"layer": "preset", "presetId": "info-preset"}
20+
EXTENSION_SOURCE = {"layer": "extension", "extensionId": "info-ext"}
1821

1922

2023
def _project(tmp_path):
@@ -126,7 +129,7 @@ def test_preset_info_json_expands_the_list_item(tmp_path, monkeypatch):
126129
{
127130
"name": "speckit.plan",
128131
"description": "Wrapped plan",
129-
"source": SOURCE,
132+
"source": PRESET_SOURCE,
130133
"sourcePath": "commands/plan.md",
131134
"strategy": "wrap",
132135
}
@@ -135,7 +138,7 @@ def test_preset_info_json_expands_the_list_item(tmp_path, monkeypatch):
135138
{
136139
"name": "spec-template",
137140
"description": "",
138-
"source": SOURCE,
141+
"source": PRESET_SOURCE,
139142
"sourcePath": "templates/spec.md",
140143
"strategy": "replace",
141144
}
@@ -144,14 +147,28 @@ def test_preset_info_json_expands_the_list_item(tmp_path, monkeypatch):
144147
{
145148
"name": "setup-plan",
146149
"description": "",
147-
"source": SOURCE,
150+
"source": PRESET_SOURCE,
148151
"sourcePath": "scripts/setup-plan.sh",
149152
"strategy": "wrap",
150153
}
151154
]
152155

153156

154157

158+
def test_preset_info_json_gives_an_empty_description_as_a_string(tmp_path, monkeypatch):
159+
# ``description:`` with no value reads as None; the preset manifest accepts it
160+
project = _project(tmp_path)
161+
_preset(project)
162+
manifest = project / ".specify" / "presets" / "info-preset" / "preset.yml"
163+
text = manifest.read_text(encoding="utf-8")
164+
manifest.write_text(text.replace(" description: Wrapped plan\n", " description:\n"), encoding="utf-8")
165+
monkeypatch.chdir(project)
166+
167+
info = _json_result(runner.invoke(app, ["preset", "info", "info-preset", "--json"]))
168+
169+
assert info["commands"][0]["description"] == ""
170+
171+
155172
def test_a_direct_call_to_preset_info_keeps_the_human_readable_view(tmp_path, monkeypatch, capsys):
156173
from specify_cli.presets.command_info import preset_info
157174

@@ -182,22 +199,22 @@ def test_extension_info_json_expands_the_list_item(tmp_path, monkeypatch):
182199
{
183200
"name": "speckit.info-ext.check",
184201
"description": "Run the check",
185-
"source": SOURCE,
202+
"source": EXTENSION_SOURCE,
186203
"sourcePath": "commands/check.md",
187204
}
188205
]
189206
assert info["templates"] == [
190-
{"name": "report", "description": "", "source": SOURCE, "sourcePath": "templates/report.md"}
207+
{"name": "report", "description": "", "source": EXTENSION_SOURCE, "sourcePath": "templates/report.md"}
191208
]
192209
assert info["scripts"] == [
193210
{
194211
"name": "collect",
195212
"description": "",
196-
"source": SOURCE,
213+
"source": EXTENSION_SOURCE,
197214
"sourcePath": "scripts/collect.sh",
198215
"runtimes": ["bash", "python"],
199216
},
200-
{"name": "notify", "description": "", "source": SOURCE, "sourcePath": "scripts/notify.sh"},
217+
{"name": "notify", "description": "", "source": EXTENSION_SOURCE, "sourcePath": "scripts/notify.sh"},
201218
]
202219

203220

0 commit comments

Comments
 (0)