mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-08 02:14:37 +00:00
feat(slash-commands): .scarftemplate format extension + catalog validator (Phase 1.8-1.9)
Slash commands now travel with .scarftemplate bundles. Schema bumps to v3 when a manifest declares contents.slashCommands; v1/v2 bundles keep parsing unchanged. Swift side: - TemplateContents gains slashCommands: [String]? — names only. Bundle layout: slash-commands/<name>.md at the root. - ProjectTemplateService.buildInstallPlan copies each claimed name into <projectDir>/.scarf/slash-commands/<name>.md. - ProjectTemplateService.verifyClaims cross-checks: each name must pass ProjectSlashCommand.validateName, the file must exist, and the bundle can't contain unclaimed slash-commands/ files. - TemplateLock gains slashCommandFiles: [String]? (relative to project root). The uninstaller's existing tracked-file logic removes them; user-authored slash commands in the same dir survive (they're not in the lock). - ProjectTemplateExporter scans <project>/.scarf/slash-commands/ on export and copies each .md into the bundle root, populating the manifest contents claim. SchemaVersion bumps to 3 only when slash commands are present. Python catalog validator (tools/build-catalog.py): - SUPPORTED_SCHEMA_VERSIONS gains 3. - SLASH_COMMAND_NAME_RE mirrors the Swift validation pattern. - _validate_contents_claim picks up slashCommands: rejects malformed names, missing files, and unclaimed extras with the same error shapes the Swift verifier uses. Tests: - 4 new test_build_catalog cases. 28/28 catalog tests pass. - ProjectTemplateTests literal updated for the new TemplateContents field. Verified: Mac + iOS builds succeed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+46
-1
@@ -47,7 +47,13 @@ from typing import Iterable
|
||||
|
||||
SCHEMA_VERSION_V1 = 1 # original v2.2 bundle
|
||||
SCHEMA_VERSION_V2 = 2 # v2.3 — adds optional manifest.config block
|
||||
SUPPORTED_SCHEMA_VERSIONS = {SCHEMA_VERSION_V1, SCHEMA_VERSION_V2}
|
||||
SCHEMA_VERSION_V3 = 3 # v2.5 — adds optional contents.slashCommands block
|
||||
SUPPORTED_SCHEMA_VERSIONS = {SCHEMA_VERSION_V1, SCHEMA_VERSION_V2, SCHEMA_VERSION_V3}
|
||||
|
||||
# Slash command names: lowercase letters, digits, hyphens; must start
|
||||
# with a letter. Mirrors `ProjectSlashCommand.validNamePattern` on the
|
||||
# Swift side so catalog ↔ installer round-trip is byte-clean.
|
||||
SLASH_COMMAND_NAME_RE = re.compile(r"^[a-z][a-z0-9-]*$")
|
||||
MAX_BUNDLE_BYTES = 5 * 1024 * 1024 # 5 MB cap on submissions; installer is 50 MB
|
||||
REQUIRED_BUNDLE_FILES = ("template.json", "README.md", "AGENTS.md", "dashboard.json")
|
||||
SUPPORTED_WIDGET_TYPES = {"stat", "progress", "text", "table", "chart", "list", "webview"}
|
||||
@@ -258,6 +264,45 @@ def _validate_contents_claim(
|
||||
f"contents.config={claimed_config} but config.schema has {schema_field_count} field(s)"
|
||||
))
|
||||
|
||||
# Slash commands (schemaVersion 3+) — each claimed name must match
|
||||
# SLASH_COMMAND_NAME_RE and have a corresponding `slash-commands/<n>.md`
|
||||
# file at the bundle root. Extra slash-commands/ files not in the
|
||||
# claim list are rejected. Mirrors Swift's verifyClaims slash-command
|
||||
# check.
|
||||
claimed_slash = contents.get("slashCommands")
|
||||
if claimed_slash is not None:
|
||||
if not isinstance(claimed_slash, list):
|
||||
errors.append(ValidationError(
|
||||
template_dir,
|
||||
"contents.slashCommands must be a list of names"
|
||||
))
|
||||
claimed_slash = []
|
||||
claimed_full = set()
|
||||
for name in claimed_slash:
|
||||
if not isinstance(name, str) or not SLASH_COMMAND_NAME_RE.match(name):
|
||||
errors.append(ValidationError(
|
||||
template_dir,
|
||||
f"contents.slashCommands name {name!r} must match {SLASH_COMMAND_NAME_RE.pattern}"
|
||||
))
|
||||
continue
|
||||
path = f"slash-commands/{name}.md"
|
||||
claimed_full.add(path)
|
||||
if path not in bundle_files:
|
||||
errors.append(ValidationError(
|
||||
template_dir,
|
||||
f"contents.slashCommands claims {name!r} but {path} is missing from the bundle"
|
||||
))
|
||||
for present in {f for f in bundle_files if f.startswith("slash-commands/")} - claimed_full:
|
||||
errors.append(ValidationError(
|
||||
template_dir,
|
||||
f"bundle has {present} but it's not listed in contents.slashCommands"
|
||||
))
|
||||
elif any(f.startswith("slash-commands/") for f in bundle_files):
|
||||
errors.append(ValidationError(
|
||||
template_dir,
|
||||
"bundle contains slash-commands/ files but contents.slashCommands is missing"
|
||||
))
|
||||
|
||||
|
||||
def _validate_config_schema(manifest: dict, template_dir: Path, errors: list[ValidationError]) -> None:
|
||||
"""Mirrors Swift `ProjectConfigService.validateSchema`. Structural
|
||||
|
||||
Reference in New Issue
Block a user