feat(factory): add native Droid plugin packaging (#120)
* feat(factory): add native Droid plugin packaging * test(factory): guard documented install contract * fix(factory): enforce install command order
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "diagram-design",
|
"name": "diagram-design",
|
||||||
"description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.",
|
"description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.",
|
||||||
"version": "2.5.13",
|
"version": "2.5.14",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Cathryn Lavery",
|
"name": "Cathryn Lavery",
|
||||||
"url": "https://github.com/cathrynlavery"
|
"url": "https://github.com/cathrynlavery"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "diagram-design",
|
"name": "diagram-design",
|
||||||
"description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.",
|
"description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.",
|
||||||
"version": "2.5.13",
|
"version": "2.5.14",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Cathryn Lavery",
|
"name": "Cathryn Lavery",
|
||||||
"url": "https://github.com/cathrynlavery"
|
"url": "https://github.com/cathrynlavery"
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "diagram-design",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "diagram-design",
|
||||||
|
"source": "./"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "diagram-design",
|
||||||
|
"description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.",
|
||||||
|
"version": "2.5.14",
|
||||||
|
"author": {
|
||||||
|
"name": "Cathryn Lavery",
|
||||||
|
"url": "https://github.com/cathrynlavery"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/cathrynlavery/diagram-design",
|
||||||
|
"repository": "https://github.com/cathrynlavery/diagram-design",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"diagrams",
|
||||||
|
"svg",
|
||||||
|
"architecture",
|
||||||
|
"flowchart",
|
||||||
|
"visualization",
|
||||||
|
"editorial",
|
||||||
|
"drawio",
|
||||||
|
"mermaid",
|
||||||
|
"import",
|
||||||
|
"animation",
|
||||||
|
"semantic-patterns"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
*New in 2.5.10: ten more layout grammars — Sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, and database schema.*
|
*New in 2.5.10: ten more layout grammars — Sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, and database schema.*
|
||||||
|
|
||||||
38 editorial diagram types for Claude Code, Codex, and Pi. Self-contained HTML + SVG. No shadows. No Mermaid slop. Semantic patterns describe behavior separately from layout, so a queue, policy trace, or trust boundary can use the nearest existing type without expanding the type count. Static HTML remains the default; optional motion is available for ordered explanations. The skill also redraws draw.io or Mermaid sources at a chosen format, size, and detail level.
|
38 editorial diagram types for Claude Code, Codex, Factory Droid, and Pi. Self-contained HTML + SVG. No shadows. No Mermaid slop. Semantic patterns describe behavior separately from layout, so a queue, policy trace, or trust boundary can use the nearest existing type without expanding the type count. Static HTML remains the default; optional motion is available for ordered explanations. The skill also redraws draw.io or Mermaid sources at a chosen format, size, and detail level.
|
||||||
|
|
||||||
No Figma. No generic rounded boxes. No 30-minute color-picking sessions.
|
No Figma. No generic rounded boxes. No 30-minute color-picking sessions.
|
||||||
|
|
||||||
@@ -128,6 +128,15 @@ codex plugin add diagram-design@diagram-design
|
|||||||
|
|
||||||
Codex refreshes configured Git marketplaces at startup. To fetch immediately, run `codex plugin marketplace upgrade diagram-design` and start a new session.
|
Codex refreshes configured Git marketplaces at startup. To fetch immediately, run `codex plugin marketplace upgrade diagram-design` and start a new session.
|
||||||
|
|
||||||
|
**Factory Droid:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
droid plugin marketplace add https://github.com/cathrynlavery/diagram-design
|
||||||
|
droid plugin install diagram-design@diagram-design --scope user
|
||||||
|
```
|
||||||
|
|
||||||
|
Droid tracks Git plugins by commit rather than the manifest's display version. To fetch a merged update, run `droid plugin marketplace update diagram-design`, then `droid plugin update diagram-design@diagram-design --scope user`, and start a new session.
|
||||||
|
|
||||||
**Claude Cowork (organization marketplace):** Organization GitHub marketplaces currently require a private or internal repository, so first mirror this public repository into one owned by your organization. In **Organization settings → Plugins**, choose **Add plugin → GitHub**, connect that mirror, and enable **Sync automatically** from the marketplace menu. Automatic sync runs when a pull request containing a plugin version bump is merged to the mirror's default branch; direct pushes do not trigger the webhook. Install Diagram Design from the resulting organization marketplace.
|
**Claude Cowork (organization marketplace):** Organization GitHub marketplaces currently require a private or internal repository, so first mirror this public repository into one owned by your organization. In **Organization settings → Plugins**, choose **Add plugin → GitHub**, connect that mirror, and enable **Sync automatically** from the marketplace menu. Automatic sync runs when a pull request containing a plugin version bump is merged to the mirror's default branch; direct pushes do not trigger the webhook. Install Diagram Design from the resulting organization marketplace.
|
||||||
|
|
||||||
**Pi:**
|
**Pi:**
|
||||||
@@ -154,7 +163,7 @@ pi install ~/code/diagram-design
|
|||||||
ln -s ~/code/diagram-design/skills/diagram-design ~/.claude/skills/diagram-design
|
ln -s ~/code/diagram-design/skills/diagram-design ~/.claude/skills/diagram-design
|
||||||
```
|
```
|
||||||
|
|
||||||
The shared skill lives at `skills/diagram-design/`. Pi discovers it through the repo's standard `skills/` package directory; Claude Code, Codex, and other Agent Skills-compatible tools use the same files.
|
The shared skill lives at `skills/diagram-design/`. Pi discovers it through the repo's standard `skills/` package directory; Claude Code, Codex, Factory Droid, and other Agent Skills-compatible tools use the same files.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -218,7 +227,7 @@ See [`skills/diagram-design/references/onboarding.md`](skills/diagram-design/ref
|
|||||||
|
|
||||||
Onboard a brand once, save the result as a named profile, then add a `.diagram-design` marker containing `profile: <slug>` to each client project. Marker projects read `~/.diagram-design/profiles/<slug>.md` directly, so parallel workspaces can use different brands without overwriting a shared installed `style-guide.md`.
|
Onboard a brand once, save the result as a named profile, then add a `.diagram-design` marker containing `profile: <slug>` to each client project. Marker projects read `~/.diagram-design/profiles/<slug>.md` directly, so parallel workspaces can use different brands without overwriting a shared installed `style-guide.md`.
|
||||||
|
|
||||||
The profile library is shared across Claude Code, Codex, and Pi. Use `/diagram-design:profile` in Claude Code, `/profile` in Pi, or ask in natural language in any host. See [`profiles.md`](skills/diagram-design/references/profiles.md) for the storage, marker, and recovery contract.
|
The profile library is shared across Claude Code, Codex, Factory Droid, and Pi. Use `/diagram-design:profile` in Claude Code, `/profile` in Factory Droid or Pi, or ask in natural language in any host. See [`profiles.md`](skills/diagram-design/references/profiles.md) for the storage, marker, and recovery contract.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -229,7 +238,7 @@ The profile library is shared across Claude Code, Codex, and Pi. Use `/diagram-d
|
|||||||
open skills/diagram-design/assets/index.html # macOS
|
open skills/diagram-design/assets/index.html # macOS
|
||||||
xdg-open skills/diagram-design/assets/index.html # Linux
|
xdg-open skills/diagram-design/assets/index.html # Linux
|
||||||
|
|
||||||
# In Claude Code, Codex, or Pi, ask:
|
# In Claude Code, Codex, Factory Droid, or Pi, ask:
|
||||||
# "Make me an architecture diagram of my app: frontend, backend, database, Redis cache."
|
# "Make me an architecture diagram of my app: frontend, backend, database, Redis cache."
|
||||||
# "I need a quadrant showing Q2 projects by impact vs effort."
|
# "I need a quadrant showing Q2 projects by impact vs effort."
|
||||||
# "Give me a sequence of a bearer call with token refresh on 401."
|
# "Give me a sequence of a bearer call with token refresh on 401."
|
||||||
@@ -343,11 +352,12 @@ diagram-design/
|
|||||||
├── .agents/plugins/marketplace.json — Codex marketplace catalog
|
├── .agents/plugins/marketplace.json — Codex marketplace catalog
|
||||||
├── .claude-plugin/ — Claude marketplace + plugin manifest
|
├── .claude-plugin/ — Claude marketplace + plugin manifest
|
||||||
├── .codex-plugin/ — Codex plugin manifest
|
├── .codex-plugin/ — Codex plugin manifest
|
||||||
|
├── .factory-plugin/ — Factory Droid marketplace + plugin manifest
|
||||||
├── commands/
|
├── commands/
|
||||||
│ ├── export-diagram.md — Claude Code export command
|
│ ├── export-diagram.md — plugin export command
|
||||||
│ ├── import-drawio.md — Claude Code draw.io import command
|
│ ├── import-drawio.md — plugin draw.io import command
|
||||||
│ ├── import-mermaid.md — Claude Code Mermaid import command
|
│ ├── import-mermaid.md — plugin Mermaid import command
|
||||||
│ └── profile.md — Claude Code client-profile command
|
│ └── profile.md — plugin client-profile command
|
||||||
├── prompts/
|
├── prompts/
|
||||||
│ ├── export-diagram.md — Pi `/export-diagram` prompt template
|
│ ├── export-diagram.md — Pi `/export-diagram` prompt template
|
||||||
│ ├── import-mermaid.md — Pi Mermaid import prompt template
|
│ ├── import-mermaid.md — Pi Mermaid import prompt template
|
||||||
@@ -407,7 +417,7 @@ diagram-design/
|
|||||||
│ ├── example-policy-trace-animated.html
|
│ ├── example-policy-trace-animated.html
|
||||||
│ └── example-sequence-oauth*.html
|
│ └── example-sequence-oauth*.html
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ ├── bump-plugin-version.py — synchronized Claude/Codex version bump
|
│ ├── bump-plugin-version.py — synchronized Claude/Codex/Factory version bump
|
||||||
│ ├── render-canonical-screenshots.py — deterministic 38-type PNG catalog renderer
|
│ ├── render-canonical-screenshots.py — deterministic 38-type PNG catalog renderer
|
||||||
│ ├── verify-screenshot-freshness.py — source + screenshot digest gate
|
│ ├── verify-screenshot-freshness.py — source + screenshot digest gate
|
||||||
│ ├── verify-plugin-package.py — version + marketplace package gate
|
│ ├── verify-plugin-package.py — version + marketplace package gate
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# ADR 0008 — Native host manifests share one plugin root
|
||||||
|
|
||||||
|
**Status:** accepted (v2.5.14)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Diagram Design serves Claude Code, Codex, Pi, and Factory Droid. Factory can translate the Claude plugin layout, but relying on that fallback leaves Droid installation undocumented and outside the package-version gate. Copying the skill or commands into host-specific directories would create multiple sources of truth.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Claude, Codex, and Factory each receive the smallest native manifest and marketplace metadata their host needs. Every marketplace resolves to the repository root, where all three hosts reuse `skills/diagram-design/` and `commands/` without duplication. Pi continues to use the same root package surfaces.
|
||||||
|
|
||||||
|
The three native plugin manifests carry identical shared identity, description, version, author, repository, license, and keyword metadata. The package verifier rejects drift, deletion, unsafe marketplace paths, or a version that does not advance from the base ref. A newly tracked native manifest may be absent at the base ref only during bootstrap: its current metadata and version must match the established manifests, and those established manifests must advance.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Each native host has an explicit install path while diagram behavior remains single-sourced. Adding another host requires native metadata, package-gate coverage, documentation, and a synchronized version bump; it never justifies copying the skill or command surface. Git-based Factory installs are updated by marketplace commit, while the synchronized manifest version remains release metadata and a review gate.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Increment the synchronized Claude and Codex plugin manifest versions."""
|
"""Increment the synchronized Claude, Codex, and Factory manifest versions."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ ROOT = Path(__file__).resolve().parent.parent
|
|||||||
MANIFEST_PATHS = (
|
MANIFEST_PATHS = (
|
||||||
Path(".claude-plugin/plugin.json"),
|
Path(".claude-plugin/plugin.json"),
|
||||||
Path(".codex-plugin/plugin.json"),
|
Path(".codex-plugin/plugin.json"),
|
||||||
|
Path(".factory-plugin/plugin.json"),
|
||||||
)
|
)
|
||||||
SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
|
SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ def bump(root: Path, part: str = "patch") -> str:
|
|||||||
if any(version != raw_versions[0] for version in raw_versions[1:]):
|
if any(version != raw_versions[0] for version in raw_versions[1:]):
|
||||||
rendered = ", ".join(
|
rendered = ", ".join(
|
||||||
f"{relative}={version!r}"
|
f"{relative}={version!r}"
|
||||||
for (relative, _), version in zip(manifests, raw_versions, strict=True)
|
for (relative, _), version in zip(manifests, raw_versions)
|
||||||
)
|
)
|
||||||
raise PackageVersionError(f"manifest versions are not synchronized: {rendered}")
|
raise PackageVersionError(f"manifest versions are not synchronized: {rendered}")
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ def bump(root: Path, part: str = "patch") -> str:
|
|||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Increment both plugin manifests (patch by default)."
|
description="Increment all plugin manifests (patch by default)."
|
||||||
)
|
)
|
||||||
group = parser.add_mutually_exclusive_group()
|
group = parser.add_mutually_exclusive_group()
|
||||||
group.add_argument("--major", action="store_true", help="increment the major version")
|
group.add_argument("--major", action="store_true", help="increment the major version")
|
||||||
@@ -90,7 +91,7 @@ def main() -> int:
|
|||||||
except PackageVersionError as exc:
|
except PackageVersionError as exc:
|
||||||
print(f"ERROR: {exc}", file=sys.stderr)
|
print(f"ERROR: {exc}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
print(f"Updated Claude and Codex plugin manifests to {version}")
|
print(f"Updated Claude, Codex, and Factory plugin manifests to {version}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import tempfile
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Iterator
|
from typing import Iterator, Optional
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
VERIFY_SCRIPT = ROOT / "scripts/verify-plugin-package.py"
|
VERIFY_SCRIPT = ROOT / "scripts/verify-plugin-package.py"
|
||||||
@@ -50,9 +50,24 @@ def manifest(version: str, codex: bool = False) -> dict:
|
|||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
def seed_package(root: Path, version: str = "1.2.3") -> None:
|
def seed_factory(root: Path, version: str) -> None:
|
||||||
|
write_json(root / ".factory-plugin/plugin.json", manifest(version))
|
||||||
|
write_json(
|
||||||
|
root / ".factory-plugin/marketplace.json",
|
||||||
|
{
|
||||||
|
"name": PLUGIN_NAME,
|
||||||
|
"plugins": [{"name": PLUGIN_NAME, "source": "./"}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def seed_package(
|
||||||
|
root: Path, version: str = "1.2.3", include_factory: bool = True
|
||||||
|
) -> None:
|
||||||
write_json(root / ".claude-plugin/plugin.json", manifest(version))
|
write_json(root / ".claude-plugin/plugin.json", manifest(version))
|
||||||
write_json(root / ".codex-plugin/plugin.json", manifest(version, codex=True))
|
write_json(root / ".codex-plugin/plugin.json", manifest(version, codex=True))
|
||||||
|
if include_factory:
|
||||||
|
seed_factory(root, version)
|
||||||
write_json(
|
write_json(
|
||||||
root / ".claude-plugin/marketplace.json",
|
root / ".claude-plugin/marketplace.json",
|
||||||
{
|
{
|
||||||
@@ -80,13 +95,16 @@ def seed_package(root: Path, version: str = "1.2.3") -> None:
|
|||||||
skill = root / "skills" / PLUGIN_NAME / "SKILL.md"
|
skill = root / "skills" / PLUGIN_NAME / "SKILL.md"
|
||||||
skill.parent.mkdir(parents=True, exist_ok=True)
|
skill.parent.mkdir(parents=True, exist_ok=True)
|
||||||
skill.write_text(f"---\nname: {PLUGIN_NAME}\n---\n", encoding="utf-8")
|
skill.write_text(f"---\nname: {PLUGIN_NAME}\n---\n", encoding="utf-8")
|
||||||
|
command = root / "commands" / "export-diagram.md"
|
||||||
|
command.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
command.write_text("Export the diagram.\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def package_repo() -> Iterator[Path]:
|
def package_repo(include_factory: bool = True) -> Iterator[Path]:
|
||||||
with tempfile.TemporaryDirectory() as scratch:
|
with tempfile.TemporaryDirectory() as scratch:
|
||||||
root = Path(scratch)
|
root = Path(scratch)
|
||||||
seed_package(root)
|
seed_package(root, include_factory=include_factory)
|
||||||
subprocess.run(["git", "init", "-q"], cwd=root, check=True)
|
subprocess.run(["git", "init", "-q"], cwd=root, check=True)
|
||||||
subprocess.run(["git", "config", "user.name", "Package Test"], cwd=root, check=True)
|
subprocess.run(["git", "config", "user.name", "Package Test"], cwd=root, check=True)
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
@@ -99,16 +117,31 @@ def package_repo() -> Iterator[Path]:
|
|||||||
yield root
|
yield root
|
||||||
|
|
||||||
|
|
||||||
def set_versions(root: Path, claude: str, codex: str) -> None:
|
def set_versions(
|
||||||
|
root: Path, claude: str, codex: str, factory: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
if factory is None:
|
||||||
|
factory = codex
|
||||||
for relative, version in (
|
for relative, version in (
|
||||||
(Path(".claude-plugin/plugin.json"), claude),
|
(Path(".claude-plugin/plugin.json"), claude),
|
||||||
(Path(".codex-plugin/plugin.json"), codex),
|
(Path(".codex-plugin/plugin.json"), codex),
|
||||||
|
(Path(".factory-plugin/plugin.json"), factory),
|
||||||
):
|
):
|
||||||
payload = json.loads((root / relative).read_text(encoding="utf-8"))
|
payload = json.loads((root / relative).read_text(encoding="utf-8"))
|
||||||
payload["version"] = version
|
payload["version"] = version
|
||||||
write_json(root / relative, payload)
|
write_json(root / relative, payload)
|
||||||
|
|
||||||
|
|
||||||
|
def set_existing_versions(root: Path, version: str) -> None:
|
||||||
|
for relative in VERIFY.MANIFEST_PATHS.values():
|
||||||
|
path = root / relative
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
payload["version"] = version
|
||||||
|
write_json(path, payload)
|
||||||
|
|
||||||
|
|
||||||
def expect_failure(label: str, errors: list[str], needle: str) -> None:
|
def expect_failure(label: str, errors: list[str], needle: str) -> None:
|
||||||
if not any(needle in error for error in errors):
|
if not any(needle in error for error in errors):
|
||||||
raise AssertionError(f"{label}: expected {needle!r}, got {errors}")
|
raise AssertionError(f"{label}: expected {needle!r}, got {errors}")
|
||||||
@@ -138,6 +171,14 @@ def test_verifier() -> None:
|
|||||||
"versions must match",
|
"versions must match",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with package_repo() as root:
|
||||||
|
set_versions(root, "1.2.4", "1.2.4", "1.2.5")
|
||||||
|
expect_failure(
|
||||||
|
"Factory version drift",
|
||||||
|
VERIFY.verify_package(root, "HEAD"),
|
||||||
|
"versions must match",
|
||||||
|
)
|
||||||
|
|
||||||
with package_repo() as root:
|
with package_repo() as root:
|
||||||
set_versions(root, "1.2", "1.2")
|
set_versions(root, "1.2", "1.2")
|
||||||
expect_failure(
|
expect_failure(
|
||||||
@@ -158,6 +199,68 @@ def test_verifier() -> None:
|
|||||||
"target does not exist",
|
"target does not exist",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with package_repo() as root:
|
||||||
|
set_versions(root, "1.2.4", "1.2.4")
|
||||||
|
marketplace_path = root / ".factory-plugin/marketplace.json"
|
||||||
|
marketplace = json.loads(marketplace_path.read_text(encoding="utf-8"))
|
||||||
|
marketplace["plugins"][0]["source"] = {
|
||||||
|
"source": "local",
|
||||||
|
"path": "./",
|
||||||
|
}
|
||||||
|
write_json(marketplace_path, marketplace)
|
||||||
|
expect_failure(
|
||||||
|
"non-native Factory marketplace source",
|
||||||
|
VERIFY.verify_package(root, "HEAD"),
|
||||||
|
"must be a non-empty local path string",
|
||||||
|
)
|
||||||
|
|
||||||
|
with package_repo() as root:
|
||||||
|
set_versions(root, "1.2.4", "1.2.4")
|
||||||
|
(root / "commands/export-diagram.md").unlink()
|
||||||
|
expect_failure(
|
||||||
|
"missing shared command surface",
|
||||||
|
VERIFY.verify_package(root, "HEAD"),
|
||||||
|
"packaged commands are missing",
|
||||||
|
)
|
||||||
|
|
||||||
|
with package_repo(include_factory=False) as root:
|
||||||
|
set_existing_versions(root, "1.2.4")
|
||||||
|
seed_factory(root, "1.2.4")
|
||||||
|
errors = VERIFY.verify_package(root, "HEAD")
|
||||||
|
if errors:
|
||||||
|
raise AssertionError(f"valid Factory bootstrap failed: {errors}")
|
||||||
|
print("OK: synchronized Factory bootstrap accepted")
|
||||||
|
|
||||||
|
with package_repo(include_factory=False) as root:
|
||||||
|
seed_factory(root, "1.2.3")
|
||||||
|
expect_failure(
|
||||||
|
"Factory bootstrap without package bump",
|
||||||
|
VERIFY.verify_package(root, "HEAD"),
|
||||||
|
"must increase",
|
||||||
|
)
|
||||||
|
|
||||||
|
with package_repo(include_factory=False) as root:
|
||||||
|
set_existing_versions(root, "1.2.4")
|
||||||
|
seed_factory(root, "1.2.4")
|
||||||
|
factory_path = root / ".factory-plugin/plugin.json"
|
||||||
|
factory = json.loads(factory_path.read_text(encoding="utf-8"))
|
||||||
|
factory["description"] = "Drifted Factory description."
|
||||||
|
write_json(factory_path, factory)
|
||||||
|
expect_failure(
|
||||||
|
"Factory bootstrap with manifest drift",
|
||||||
|
VERIFY.verify_package(root, "HEAD"),
|
||||||
|
"must match Claude",
|
||||||
|
)
|
||||||
|
|
||||||
|
with package_repo() as root:
|
||||||
|
set_versions(root, "1.2.4", "1.2.4")
|
||||||
|
(root / ".factory-plugin/plugin.json").unlink()
|
||||||
|
expect_failure(
|
||||||
|
"Factory manifest deletion",
|
||||||
|
VERIFY.verify_package(root, "HEAD"),
|
||||||
|
"could not read",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_bumper() -> None:
|
def test_bumper() -> None:
|
||||||
cases = (("patch", "1.2.4"), ("minor", "1.3.0"), ("major", "2.0.0"))
|
cases = (("patch", "1.2.4"), ("minor", "1.3.0"), ("major", "2.0.0"))
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -90,8 +91,97 @@ def main() -> int:
|
|||||||
if errors != [expected]:
|
if errors != [expected]:
|
||||||
raise AssertionError(f"stale Pi prompt was not reported: {errors}")
|
raise AssertionError(f"stale Pi prompt was not reported: {errors}")
|
||||||
|
|
||||||
|
factory_manifest = root / ".factory-plugin/plugin.json"
|
||||||
|
factory_marketplace = root / ".factory-plugin/marketplace.json"
|
||||||
|
factory_manifest.parent.mkdir(parents=True)
|
||||||
|
factory_manifest.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"name": "diagram-design",
|
||||||
|
"repository": "https://github.com/example/diagram-design",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
factory_marketplace.write_text(
|
||||||
|
json.dumps({"name": "diagram-design"}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
valid_readme = """# Diagram Design
|
||||||
|
|
||||||
|
```bash
|
||||||
|
droid plugin marketplace add https://github.com/example/diagram-design
|
||||||
|
droid plugin install diagram-design@diagram-design --scope user
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
diagram-design/
|
||||||
|
├── .factory-plugin/ — Factory Droid metadata
|
||||||
|
├── commands/
|
||||||
|
└── skills/
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
readme = root / "README.md"
|
||||||
|
readme.write_text(valid_readme, encoding="utf-8")
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
verify.check_factory_install_surface(errors, root)
|
||||||
|
if errors:
|
||||||
|
raise AssertionError(f"valid Factory install contract failed: {errors}")
|
||||||
|
|
||||||
|
readme.write_text(
|
||||||
|
valid_readme.replace(
|
||||||
|
"droid plugin marketplace add https://github.com/example/diagram-design\n"
|
||||||
|
"droid plugin install diagram-design@diagram-design --scope user",
|
||||||
|
"droid plugin install diagram-design@diagram-design --scope user\n"
|
||||||
|
"droid plugin marketplace add https://github.com/example/diagram-design",
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
errors = []
|
||||||
|
verify.check_factory_install_surface(errors, root)
|
||||||
|
expected = (
|
||||||
|
"README Factory install block must match native metadata: "
|
||||||
|
"`droid plugin marketplace add https://github.com/example/diagram-design` "
|
||||||
|
"then `droid plugin install diagram-design@diagram-design`"
|
||||||
|
)
|
||||||
|
if errors != [expected]:
|
||||||
|
raise AssertionError(
|
||||||
|
f"reversed Factory install commands were not reported: {errors}"
|
||||||
|
)
|
||||||
|
|
||||||
|
readme.write_text(
|
||||||
|
valid_readme.replace(
|
||||||
|
"diagram-design@diagram-design", "diagram-design@wrong-marketplace"
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
errors = []
|
||||||
|
verify.check_factory_install_surface(errors, root)
|
||||||
|
expected = (
|
||||||
|
"README Factory install block must match native metadata: "
|
||||||
|
"`droid plugin marketplace add https://github.com/example/diagram-design` "
|
||||||
|
"then `droid plugin install diagram-design@diagram-design`"
|
||||||
|
)
|
||||||
|
if errors != [expected]:
|
||||||
|
raise AssertionError(
|
||||||
|
f"drifted Factory plugin ID was not reported: {errors}"
|
||||||
|
)
|
||||||
|
|
||||||
|
readme.write_text(
|
||||||
|
valid_readme.replace("├── .factory-plugin/ — Factory Droid metadata\n", ""),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
errors = []
|
||||||
|
verify.check_factory_install_surface(errors, root)
|
||||||
|
expected = (
|
||||||
|
"README architecture tree must list Factory's native .factory-plugin/ path"
|
||||||
|
)
|
||||||
|
if errors != [expected]:
|
||||||
|
raise AssertionError(f"missing Factory native path was not reported: {errors}")
|
||||||
|
|
||||||
print(
|
print(
|
||||||
"PASS: docs sync checks reference links and Claude/Pi profile-surface parity"
|
"PASS: docs sync checks references, profile surfaces, and Factory install contract"
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Verify that routing and browsing surfaces stay in sync with the skill.
|
"""Verify that routing and browsing surfaces stay in sync with the skill.
|
||||||
|
|
||||||
Five drift classes, each of which has shipped before:
|
Seven drift classes, each of which has shipped before:
|
||||||
|
|
||||||
1. The SKILL.md frontmatter description is the only text an agent sees before
|
1. The SKILL.md frontmatter description is the only text an agent sees before
|
||||||
deciding to load the skill — every visual type in the selection table must
|
deciding to load the skill — every visual type in the selection table must
|
||||||
@@ -15,6 +15,8 @@ Five drift classes, each of which has shipped before:
|
|||||||
text a user reads *before installing*, so by ADR 0004's own argument they
|
text a user reads *before installing*, so by ADR 0004's own argument they
|
||||||
need every type's lexical hook too - and nothing else notices when they
|
need every type's lexical hook too - and nothing else notices when they
|
||||||
drift, because they are four separate copies of one sentence.
|
drift, because they are four separate copies of one sentence.
|
||||||
|
7. Factory Droid's README install commands and native manifest path must agree
|
||||||
|
with the package metadata instead of becoming a second hand-maintained API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -40,6 +42,8 @@ PROFILE_SURFACES = (
|
|||||||
Path("commands/profile.md"),
|
Path("commands/profile.md"),
|
||||||
Path("prompts/profile.md"),
|
Path("prompts/profile.md"),
|
||||||
)
|
)
|
||||||
|
FACTORY_MANIFEST = Path(".factory-plugin/plugin.json")
|
||||||
|
FACTORY_MARKETPLACE = Path(".factory-plugin/marketplace.json")
|
||||||
|
|
||||||
|
|
||||||
def normalized(text: str) -> str:
|
def normalized(text: str) -> str:
|
||||||
@@ -156,10 +160,56 @@ def check_profile_surfaces(errors: list[str], root: Path) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_factory_install_surface(errors: list[str], root: Path) -> None:
|
||||||
|
markdown = (root / "README.md").read_text(encoding="utf-8")
|
||||||
|
manifest = json.loads((root / FACTORY_MANIFEST).read_text(encoding="utf-8"))
|
||||||
|
marketplace = json.loads((root / FACTORY_MARKETPLACE).read_text(encoding="utf-8"))
|
||||||
|
code_blocks = re.findall(
|
||||||
|
r"^```[^\n]*\n(.*?)^```[ \t]*$", markdown, re.MULTILINE | re.DOTALL
|
||||||
|
)
|
||||||
|
|
||||||
|
marketplace_command = f"droid plugin marketplace add {manifest['repository']}"
|
||||||
|
install_command = f"droid plugin install {manifest['name']}@{marketplace['name']}"
|
||||||
|
command_blocks = [
|
||||||
|
[line.strip() for line in block.splitlines() if line.strip()]
|
||||||
|
for block in code_blocks
|
||||||
|
]
|
||||||
|
install_is_documented = any(
|
||||||
|
any(
|
||||||
|
line == install_command or line.startswith(f"{install_command} ")
|
||||||
|
for line in lines[lines.index(marketplace_command) + 1 :]
|
||||||
|
)
|
||||||
|
for lines in command_blocks
|
||||||
|
if marketplace_command in lines
|
||||||
|
)
|
||||||
|
if not install_is_documented:
|
||||||
|
errors.append(
|
||||||
|
"README Factory install block must match native metadata: "
|
||||||
|
f"`{marketplace_command}` then `{install_command}`"
|
||||||
|
)
|
||||||
|
|
||||||
|
native_directory = f"{FACTORY_MANIFEST.parent.as_posix()}/"
|
||||||
|
architecture_blocks = [
|
||||||
|
block
|
||||||
|
for block in code_blocks
|
||||||
|
if "diagram-design/" in block and "commands/" in block
|
||||||
|
]
|
||||||
|
native_path_is_documented = any(
|
||||||
|
line.lstrip(" │├─└").startswith(native_directory)
|
||||||
|
for block in architecture_blocks
|
||||||
|
for line in block.splitlines()
|
||||||
|
)
|
||||||
|
if not native_path_is_documented:
|
||||||
|
errors.append(
|
||||||
|
f"README architecture tree must list Factory's native {native_directory} path"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
MANIFEST_DESCRIPTIONS = (
|
MANIFEST_DESCRIPTIONS = (
|
||||||
(Path(".claude-plugin/plugin.json"), ("description",)),
|
(Path(".claude-plugin/plugin.json"), ("description",)),
|
||||||
(Path(".claude-plugin/marketplace.json"), ("description",)),
|
(Path(".claude-plugin/marketplace.json"), ("description",)),
|
||||||
(Path(".codex-plugin/plugin.json"), ("description", "longDescription")),
|
(Path(".codex-plugin/plugin.json"), ("description", "longDescription")),
|
||||||
|
(FACTORY_MANIFEST, ("description",)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -212,6 +262,7 @@ def main() -> int:
|
|||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
check_description(errors)
|
check_description(errors)
|
||||||
check_manifest_descriptions(errors, ROOT)
|
check_manifest_descriptions(errors, ROOT)
|
||||||
|
check_factory_install_surface(errors, ROOT)
|
||||||
check_gallery(errors)
|
check_gallery(errors)
|
||||||
check_readme_tree(errors)
|
check_readme_tree(errors)
|
||||||
check_skill_reference_links(
|
check_skill_reference_links(
|
||||||
@@ -227,7 +278,7 @@ def main() -> int:
|
|||||||
return 1
|
return 1
|
||||||
print(
|
print(
|
||||||
"OK docs sync: description hooks, gallery reachability, README tree, "
|
"OK docs sync: description hooks, gallery reachability, README tree, "
|
||||||
"reference links, profile surfaces, manifest descriptions"
|
"reference links, profile surfaces, manifest descriptions, Factory install contract"
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,21 @@ PLUGIN_NAME = "diagram-design"
|
|||||||
MANIFEST_PATHS = {
|
MANIFEST_PATHS = {
|
||||||
"Claude": Path(".claude-plugin/plugin.json"),
|
"Claude": Path(".claude-plugin/plugin.json"),
|
||||||
"Codex": Path(".codex-plugin/plugin.json"),
|
"Codex": Path(".codex-plugin/plugin.json"),
|
||||||
|
"Factory": Path(".factory-plugin/plugin.json"),
|
||||||
}
|
}
|
||||||
CLAUDE_MARKETPLACE = Path(".claude-plugin/marketplace.json")
|
CLAUDE_MARKETPLACE = Path(".claude-plugin/marketplace.json")
|
||||||
CODEX_MARKETPLACE = Path(".agents/plugins/marketplace.json")
|
CODEX_MARKETPLACE = Path(".agents/plugins/marketplace.json")
|
||||||
|
FACTORY_MARKETPLACE = Path(".factory-plugin/marketplace.json")
|
||||||
|
SHARED_MANIFEST_FIELDS = (
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"version",
|
||||||
|
"author",
|
||||||
|
"homepage",
|
||||||
|
"repository",
|
||||||
|
"license",
|
||||||
|
"keywords",
|
||||||
|
)
|
||||||
SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
|
SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")
|
||||||
|
|
||||||
|
|
||||||
@@ -36,7 +48,20 @@ def load_json(path: Path, errors: list[str]) -> dict[str, Any] | None:
|
|||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
def load_base_json(root: Path, base_ref: str, relative: Path, errors: list[str]) -> dict[str, Any] | None:
|
def base_path_exists(root: Path, base_ref: str, relative: Path) -> bool:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "cat-file", "-e", f"{base_ref}:{relative.as_posix()}"],
|
||||||
|
cwd=root,
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def load_base_json(
|
||||||
|
root: Path, base_ref: str, relative: Path, errors: list[str]
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["git", "show", f"{base_ref}:{relative.as_posix()}"],
|
["git", "show", f"{base_ref}:{relative.as_posix()}"],
|
||||||
cwd=root,
|
cwd=root,
|
||||||
@@ -125,10 +150,26 @@ def verify_versions(
|
|||||||
version_values = list(current_versions.values())
|
version_values = list(current_versions.values())
|
||||||
if any(version != version_values[0] for version in version_values[1:]):
|
if any(version != version_values[0] for version in version_values[1:]):
|
||||||
rendered = ", ".join(f"{label}={value!r}" for label, value in current_versions.items())
|
rendered = ", ".join(f"{label}={value!r}" for label, value in current_versions.items())
|
||||||
errors.append(f"Claude and Codex manifest versions must match: {rendered}")
|
errors.append(f"plugin manifest versions must match: {rendered}")
|
||||||
|
|
||||||
|
base_check = subprocess.run(
|
||||||
|
["git", "rev-parse", "--verify", f"{base_ref}^{{commit}}"],
|
||||||
|
cwd=root,
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if base_check.returncode != 0:
|
||||||
|
detail = base_check.stderr.strip() or "not a commit"
|
||||||
|
errors.append(f"could not resolve base ref {base_ref!r}: {detail}")
|
||||||
|
return
|
||||||
|
|
||||||
|
base_manifest_count = 0
|
||||||
for label, relative in MANIFEST_PATHS.items():
|
for label, relative in MANIFEST_PATHS.items():
|
||||||
current = parse_semver(current_versions.get(label), f"current {label}", errors)
|
current = parse_semver(current_versions.get(label), f"current {label}", errors)
|
||||||
|
if not base_path_exists(root, base_ref, relative):
|
||||||
|
continue
|
||||||
|
base_manifest_count += 1
|
||||||
base_manifest = load_base_json(root, base_ref, relative, errors)
|
base_manifest = load_base_json(root, base_ref, relative, errors)
|
||||||
if base_manifest is None:
|
if base_manifest is None:
|
||||||
continue
|
continue
|
||||||
@@ -138,6 +179,11 @@ def verify_versions(
|
|||||||
f"{label} manifest version must increase relative to {base_ref}: "
|
f"{label} manifest version must increase relative to {base_ref}: "
|
||||||
f"{base_manifest.get('version')} -> {current_versions.get(label)}"
|
f"{base_manifest.get('version')} -> {current_versions.get(label)}"
|
||||||
)
|
)
|
||||||
|
if base_manifest_count == 0:
|
||||||
|
errors.append(
|
||||||
|
f"no synchronized plugin manifest exists at {base_ref}; "
|
||||||
|
"cannot establish that the package version advanced"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def verify_manifest_identity(manifests: dict[str, dict[str, Any]], errors: list[str]) -> None:
|
def verify_manifest_identity(manifests: dict[str, dict[str, Any]], errors: list[str]) -> None:
|
||||||
@@ -146,20 +192,40 @@ def verify_manifest_identity(manifests: dict[str, dict[str, Any]], errors: list[
|
|||||||
errors.append(
|
errors.append(
|
||||||
f"{label} manifest name must remain {PLUGIN_NAME!r}; got {payload.get('name')!r}"
|
f"{label} manifest name must remain {PLUGIN_NAME!r}; got {payload.get('name')!r}"
|
||||||
)
|
)
|
||||||
|
reference_label = next(iter(MANIFEST_PATHS))
|
||||||
|
reference = manifests[reference_label]
|
||||||
|
for label, payload in manifests.items():
|
||||||
|
if label == reference_label:
|
||||||
|
continue
|
||||||
|
for field in SHARED_MANIFEST_FIELDS:
|
||||||
|
if payload.get(field) != reference.get(field):
|
||||||
|
errors.append(
|
||||||
|
f"{label} manifest {field!r} must match {reference_label}; "
|
||||||
|
f"got {payload.get(field)!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def verify_marketplaces(root: Path, errors: list[str]) -> None:
|
def verify_marketplaces(root: Path, errors: list[str]) -> None:
|
||||||
claude_marketplace = load_json(root / CLAUDE_MARKETPLACE, errors)
|
claude_marketplace = load_json(root / CLAUDE_MARKETPLACE, errors)
|
||||||
codex_marketplace = load_json(root / CODEX_MARKETPLACE, errors)
|
codex_marketplace = load_json(root / CODEX_MARKETPLACE, errors)
|
||||||
if claude_marketplace is None or codex_marketplace is None:
|
factory_marketplace = load_json(root / FACTORY_MARKETPLACE, errors)
|
||||||
|
if (
|
||||||
|
claude_marketplace is None
|
||||||
|
or codex_marketplace is None
|
||||||
|
or factory_marketplace is None
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
claude_entry = find_plugin_entry(claude_marketplace, "Claude", errors)
|
claude_entry = find_plugin_entry(claude_marketplace, "Claude", errors)
|
||||||
codex_entry = find_plugin_entry(codex_marketplace, "Codex", errors)
|
codex_entry = find_plugin_entry(codex_marketplace, "Codex", errors)
|
||||||
if claude_entry is None or codex_entry is None:
|
factory_entry = find_plugin_entry(factory_marketplace, "Factory", errors)
|
||||||
|
if claude_entry is None or codex_entry is None or factory_entry is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
claude_root = resolve_local_path(root, claude_entry.get("source"), "Claude plugin source", errors)
|
claude_root = resolve_local_path(root, claude_entry.get("source"), "Claude plugin source", errors)
|
||||||
|
factory_root = resolve_local_path(
|
||||||
|
root, factory_entry.get("source"), "Factory plugin source", errors
|
||||||
|
)
|
||||||
|
|
||||||
codex_source = codex_entry.get("source")
|
codex_source = codex_entry.get("source")
|
||||||
if not isinstance(codex_source, dict) or codex_source.get("source") != "local":
|
if not isinstance(codex_source, dict) or codex_source.get("source") != "local":
|
||||||
@@ -183,15 +249,26 @@ def verify_marketplaces(root: Path, errors: list[str]) -> None:
|
|||||||
errors.append("Claude marketplace target does not contain .claude-plugin/plugin.json")
|
errors.append("Claude marketplace target does not contain .claude-plugin/plugin.json")
|
||||||
if codex_root is not None and not (codex_root / MANIFEST_PATHS["Codex"]).is_file():
|
if codex_root is not None and not (codex_root / MANIFEST_PATHS["Codex"]).is_file():
|
||||||
errors.append("Codex marketplace target does not contain .codex-plugin/plugin.json")
|
errors.append("Codex marketplace target does not contain .codex-plugin/plugin.json")
|
||||||
if claude_root is not None and codex_root is not None and claude_root != codex_root:
|
if factory_root is not None and not (factory_root / MANIFEST_PATHS["Factory"]).is_file():
|
||||||
errors.append("Claude and Codex marketplaces must package the same plugin root")
|
errors.append("Factory marketplace target does not contain .factory-plugin/plugin.json")
|
||||||
|
|
||||||
plugin_root = codex_root or claude_root
|
plugin_roots = [
|
||||||
|
plugin_root
|
||||||
|
for plugin_root in (claude_root, codex_root, factory_root)
|
||||||
|
if plugin_root is not None
|
||||||
|
]
|
||||||
|
if plugin_roots and any(plugin_root != plugin_roots[0] for plugin_root in plugin_roots[1:]):
|
||||||
|
errors.append("Claude, Codex, and Factory marketplaces must package the same plugin root")
|
||||||
|
|
||||||
|
plugin_root = codex_root or claude_root or factory_root
|
||||||
if plugin_root is None:
|
if plugin_root is None:
|
||||||
return
|
return
|
||||||
skill = plugin_root / "skills" / PLUGIN_NAME / "SKILL.md"
|
skill = plugin_root / "skills" / PLUGIN_NAME / "SKILL.md"
|
||||||
if not skill.is_file():
|
if not skill.is_file():
|
||||||
errors.append(f"packaged skill is missing: {skill.relative_to(root)}")
|
errors.append(f"packaged skill is missing: {skill.relative_to(root)}")
|
||||||
|
commands = plugin_root / "commands"
|
||||||
|
if not commands.is_dir() or not any(commands.glob("*.md")):
|
||||||
|
errors.append("packaged commands are missing from the shared plugin root")
|
||||||
|
|
||||||
|
|
||||||
def verify_codex_skill_path(root: Path, codex_manifest: dict[str, Any], errors: list[str]) -> None:
|
def verify_codex_skill_path(root: Path, codex_manifest: dict[str, Any], errors: list[str]) -> None:
|
||||||
@@ -232,7 +309,7 @@ def main() -> int:
|
|||||||
return 1
|
return 1
|
||||||
versions = load_json(ROOT / MANIFEST_PATHS["Claude"], [])["version"]
|
versions = load_json(ROOT / MANIFEST_PATHS["Claude"], [])["version"]
|
||||||
print(
|
print(
|
||||||
f"OK plugin package: Claude and Codex {versions}, "
|
f"OK plugin package: Claude, Codex, and Factory {versions}, "
|
||||||
f"marketplace paths, and packaged skill"
|
f"marketplace paths, and packaged skill"
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -198,6 +198,12 @@ Use the installed-skill location exposed by the current agent when available. Ot
|
|||||||
1. `~/.claude/skills/<skill-name>/` (user install)
|
1. `~/.claude/skills/<skill-name>/` (user install)
|
||||||
2. `.claude/skills/<skill-name>/` (project install)
|
2. `.claude/skills/<skill-name>/` (project install)
|
||||||
|
|
||||||
|
**Factory Droid:**
|
||||||
|
|
||||||
|
1. `~/.factory/skills/<skill-name>/` (personal install)
|
||||||
|
2. `.factory/skills/<skill-name>/` from the current directory through the repo root (folder-specific or project install)
|
||||||
|
3. The active path shown in `/skills` under **Plugins**; installed plugins keep the shared `skills/<skill-name>/` directory inside Droid's plugin cache
|
||||||
|
|
||||||
Finally, check any path the user provides explicitly. If the skill is still not found, ask the user to confirm the name or provide its path.
|
Finally, check any path the user provides explicitly. If the skill is still not found, ask the user to confirm the name or provide its path.
|
||||||
|
|
||||||
### Step 2 — read token sources
|
### Step 2 — read token sources
|
||||||
|
|||||||
Reference in New Issue
Block a user