skill-portability
Make any plugin fully portable across all platforms. Accepts Claude, Cursor, Gemini, OpenCode, or bare SKILL.md repos as input. Emits every missing platform artifact.
cursor.directory·↓ 2
Skill
assessing-plugin-portability
Assess a plugin repo and report portability gaps across all platforms. Just generates a portability report. Makes no changes to your code.
# Assessing Plugin Portability
Assess a plugin repo and report portability gaps across all platforms. Makes no changes.
**Input:** `plugin_path` (string, required) — Path to the plugin root directory.
**Output:** Complete portability assessment with per-platform scores and recommendation.
> **Detection Algorithm:** `lib/patterns/detection-algorithm.md`
> **Rubric Framework:** `lib/patterns/rubric-framework.md`
> **Platform Rules:** `lib/patterns/platforms/<platform>.md`
> **Injection Checks:** `lib/patterns/injection-checks.md`
---
## Overview
| Phase | Description |
| ----- | ----------- |
| **Phase 1: Detect** | Scan metadata, elect canonical, build model, classify shape |
| **Phase 2: Inventory** | Discover all assets across all platform conventions |
| **Phase 3: Score** | Run per-platform rubric, detect blockers |
| **Phase 4: Recommend** | Choose uplift target, per-platform recommendations |
| **Phase 5: Report** | Print full assessment report |
**Minimum starting state:** At least one `skills/*/SKILL.md` with frontmatter, or any platform manifest file.
---
## Phase 1: Detect
### Step 1.1: Scan and Infer
```pseudocode
DETECT(plugin_path):
computed.sources = scan_metadata_sources(plugin_path) # D1
IF len(computed.sources) == 0:
DISPLAY "No recognisable plugin signals found in {plugin_path}."
EXIT
computed.canonical = elect_canonical(computed.sources) # D2
computed.metadata = build_metadata_model(computed.sources) # D3
print_inference_summary(computed.metadata, computed.canonical) # D4
computed.shape = classify_shape(computed.sources) # D5
```
---
## Phase 2: Inventory
### Step 2.1: Check Platform Manifests
Check all 10 manifest paths across 6 platforms, recording `{ platform, path, status }`:
```pseudocode
INVENTORY_MANIFESTS(computed):
manifest_checks = [
{ platform: "claude-code", path: ".claude-plugin/plugin.json" },
{ platform: "claude-code", path: ".claude-plugin/marketplace.json" },
{ platform: "cursor", path: ".cursor-plugin/plugin.json" },
{ platform: "gemini-cli", path: "gemini-extension.json" },
{ platform: "gemini-cli", path: "GEMINI.md" },
{ platform: "opencode", path: ".opencode/plugins/" + computed.metadata.name + ".js" },
{ platform: "codex", path: ".codex-plugin/plugin.json" },
{ platform: "codex", path: ".agents/plugins/marketplace.json" },
{ platform: "copilot-cli", path: "package.json" },
{ platform: "copilot-cli", path: ".github/copilot-instructions.md" }
]
computed.manifest_results = []
FOR check IN manifest_checks:
status = IF file_exists(plugin_path + "/" + check.path) THEN "PRESENT" ELSE "MISSING"
computed.manifest_results.append({ platform: check.platform, path: check.path, status: status })
```
### Step 2.2: Check Context Files
```pseudocode
INVENTORY_CONTEXT_FILES(computed):
context_checks = [
"CLAUDE.md",
"AGENTS.md",
"GEMINI.md",
".github/copilot-instructions.md",
".codex/INSTALL.md"
]
computed.context_results = []
FOR path IN context_checks:
status = IF file_exists(plugin_path + "/" + path) THEN "PRESENT" ELSE "MISSING"
computed.context_results.append({ path: path, status: status })
```
### Step 2.3: Check Per-Skill Sidecars
Use `skill.dir` (directory basename) — not `skill.name` (frontmatter value) — for path construction.
```pseudocode
INVENTORY_SIDECARS(computed):
computed.skills = Glob(plugin_path + "/skills/*/SKILL.md")
sidecar_files = ["copilot-tools.md", "codex-tools.md", "gemini-tools.md"]
computed.sidecar_results = []
FOR skill IN computed.skills:
FOR sidecar IN sidecar_files:
target = "skills/" + skill.dir + "/references/" + sidecar
status = IF file_exists(plugin_path + "/" + target) THEN "PRESENT" ELSE "MISSING"
computed.sidecar_results.append({ skill: skill.dir, file: sidecar, status: status })
```
### Step 2.4: Check Hooks
```pseudocode
INVENTORY_HOOKS(computed):
hook_checks = [
"hooks/hooks.json",
"hooks/hooks-cursor.json",
".github/hooks/",
"hooks/run-hook.cmd"
]
computed.hook_results = []
FOR path IN hook_checks:
status = IF file_exists(plugin_path + "/" + path) THEN "PRESENT" ELSE "MISSING"
computed.hook_results.append({ path: path, status: status })
```
### Step 2.5: Check Frontmatter Compatibility
```pseudocode
INVENTORY_FRONTMATTER(computed):
computed.frontmatter_results = []
FOR skill IN computed.skills:
frontmatter = parse_yaml_frontmatter(skill.content)
IF frontmatter.name AND frontmatter.description:
status = "COMPATIBLE"
ELSE:
status = "MISSING FRONTMATTER"
computed.frontmatter_results.append({ skill: skill.dir, status: status })
```
### Step 2.6: Check Session-Start Injection
See `lib/patterns/injection-checks.md` for the 8-component verification.
```pseudocode
INVENTORY_INJECTION(computed):
using_path = "skills/using-" + computed.metadata.name + "/SKILL.md"
IF NOT file_exists(plugin_path + "/" + using_path):
computed.injection_status = "NOT CONFIGURED"
RETURN
computed.injection_results = check_injection_components(computed)
computed.injection_summary = compute_injection_summary(computed.injection_results)
```
### Step 2.7: Check Context File Completeness
```pseudocode
INVENTORY_CONTEXT_COMPLETENESS(computed):
computed.completeness_issues = []
IF file_exists(plugin_path + "/GEMINI.md"):
content = Read(plugin_path + "/GEMINI.md")
FOR skill IN computed.skills:
IF "@./skills/" + skill.dir + "/SKILL.md" NOT IN content:
computed.completeness_issues.append("GEMINI.md missing @include for " + skill.dir)
IF "@./skills/" + skill.dir + "/references/gemini-tools.md" NOT IN content:
computed.completeness_issues.append("GEMINI.md missing gemini-tools include for " + skill.dir)
ELSE:
computed.completeness_issues.append("GEMINI.md: MISSING — cannot check includes")
IF file_exists(plugin_path + "/AGENTS.md"):
content = Read(plugin_path + "/AGENTS.md")
FOR skill IN computed.skills:
IF "skills/" + skill.dir + "/SKILL.md" NOT IN content:
computed.completeness_issues.append("AGENTS.md missing reference for " + skill.dir)
ELSE:
computed.completeness_issues.append("AGENTS.md: MISSING — cannot check skill references")
# Copilot-specific
computed.copilot_checks = [
Glob(plugin_path + "/.github/instructions/*.instructions.md"),
Glob(plugin_path + "/.github/agents/*.agent.md")
]
# Gemini-specific
computed.gemini_checks = [
Glob(plugin_path + "/commands/*.toml"),
Glob(plugin_path + "/policies/*.toml")
]
# MCP configs
computed.mcp_checks = []
FOR path IN [".mcp.json", "mcp.json"]:
IF file_exists(plugin_path + "/" + path):
computed.mcp_checks.append({ path: path, status: "PRESENT" })
```
---
## Phase 3: Score
### Step 3.1: Run Per-Platform Rubric
See `lib/patterns/rubric-framework.md` for category definitions and scoring scale (0-3 per category, max 21 per platform).
```pseudocode
SCORE(computed):
platforms = ["claude-code", "cursor", "gemini-cli", "opencode", "copilot-cli", "codex"]
FOR platform IN platforms:
rules = load_platform_rules(platform) # from lib/patterns/platforms/
computed.scores[platform] = {}
FOR category IN rules.categories:
computed.scores[platform][category.name] = evaluate(category, computed)
computed.scores[platform].total = sum(computed.scores[platform][c] for c IN rules.categories)
computed.scores[platform].band = classify_band(computed.scores[platform].total)
```
### Step 3.2: Detect Blockers
```pseudocode
DETECT_BLOCKERS(computed):
computed.blockers = []
# Critical: no trustworthy metadata
IF all metadata fields derived from hard fallbacks only:
computed.blockers.append({
severity: "critical",
description: "No trustworthy metadata source — all fields from hard fallbacks"
})
# Major: unresolved tool assumptions
FOR skill IN computed.skills:
IF skill references platform-specific tools AND skill.dir not in sidecar_results with status PRESENT:
computed.blockers.append({
severity: "major",
description: "Unresolved tool assumptions in skills/" + skill.dir
})
# Major: hook env hard-coding
FOR hook_file IN ["hooks/hooks.json", "hooks/hooks-cursor.json"]:
IF file_exists(hook_file):
content = Read(plugin_path + "/" + hook_file)
IF "CLAUDE_PLUGIN_ROOT" IN content AND env_branching_absent(content):
computed.blockers.append({
severity: "major",
description: "Hook env hard-coding in " + hook_file + " (no env branching)"
})
# Major: docs/structure mismatch
IF install_docs_reference_paths_that_dont_exist(computed):
computed.blockers.append({
severity: "major",
description: "Install docs describe paths that don't exist in repo"
})
# Minor: GEMINI.md import gaps
gemini_gaps = [i for i IN computed.completeness_issues if "GEMINI.md missing" IN i]
IF len(gemini_gaps) > 0:
computed.blockers.append({
severity: "minor",
description: "GEMINI.md import gaps: " + str(len(gemini_gaps)) + " missing includes"
})
```
---
## Phase 4: Recommend
### Step 4.1: Choose Uplift Target
```pseudocode
RECOMMEND_TARGET(computed):
IF computed.shape == "bare-skill-repo":
IF len(computed.skills) <= 3:
computed.recommendation = "skill-first"
ELSE:
computed.recommendation = "full-portable-plugin"
ELIF computed.shape == "single-platform-plugin":
computed.recommendation = "full-portable-plugin"
ELIF computed.shape == "multi-platform-source":
computed.recommendation = "full-portable-plugin"
ELIF computed.shape == "curated-distribution":
computed.recommendation = "curated-note-only"
ELSE:
computed.recommendation = "full-portable-plugin"
```
### Step 4.2: Choose Codex Path
```pseudocode
RECOMMEND_CODEX(computed):
IF ".codex-plugin/plugin.json" PRESENT in computed.manifest_results:
computed.codex_rec = "native-plugin-packaging"
ELIF computed.shape == "bare-skill-repo" AND len(computed.skills) > 0:
computed.codex_rec = "native-skill-discovery"
ELIF computed.shape IN ["single-platform-plugin", "multi-platform-source"]:
computed.codex_rec = "native-plugin-packaging"
ELIF computed.shape == "curated-distribution":
computed.codex_rec = "curated-package-note"
ELSE:
computed.codex_rec = "native-plugin-packaging"
```
### Step 4.3: Per-Platform Action Summary
```pseudocode
SUMMARISE_ACTIONS(computed):
computed.actions = {}
FOR platform IN platforms:
band = computed.scores[platform].band
IF band == "strong":
computed.actions[platform] = "No action required"
ELIF band == "viable":
computed.actions[platform] = "Minor gaps — review category detail"
ELIF band == "partial":
computed.actions[platform] = "Significant gaps — uplift recommended"
ELSE: # weak
computed.actions[platform] = "Full uplift required"
```
---
## Phase 5: Report
### Step 5.1: Print Assessment
```text
# Portability Assessment: {name} v{version}
## Repo Shape
{shape}
Metadata inferred from: {canonical.path}
## Platform Scores
| Platform | Score | Band | Action |
|-------------|-------|---------|------------------------------------|
| claude-code | X/21 | {band} | {action} |
| cursor | X/21 | {band} | {action} |
| gemini-cli | X/21 | {band} | {action} |
| opencode | X/21 | {band} | {action} |
| copilot-cli | X/21 | {band} | {action} |
| codex | X/21 | {band} | {action} |
### Per-Platform Detail
#### {platform}
| Category | Score |
|---------------------|-------|
| Manifest packaging | X/3 |
| Skill compatibility | X/3 |
| Context delivery | X/3 |
| Hook portability | X/3 |
| Tool mapping | X/3 |
| Install readiness | X/3 |
| Runtime adapters | X/3 |
| **Total** | X/21 |
(repeat for each platform)
## Blockers
{severity}: {description}
(one entry per blocker; "None detected." if empty)
## Uplift Recommendation
Target: {recommendation}
Codex path: {codex_rec}
## Required Artifacts
(per platform where band != strong: list missing artifacts)
### {platform} — {band}
- {missing artifact path}
- ...
## Session-Start Injection
{status}
(IF configured: component-by-component status table)
## Summary
Run the uplifting-a-plugin skill to generate all missing artifacts automatically.
```
---
## State Flow
```text
Phase 1 Phase 2 Phase 3
─────────────────────────────────────────────────────────────
computed computed.manifest_results computed.scores[platform]
.sources .context_results .blockers
.canonical .sidecar_results
.metadata .hook_results
.shape .frontmatter_results
.injection_results
.completeness_issues
.copilot_checks
.gemini_checks
.mcp_checks
Phase 4 Phase 5
──────────────────────────────
computed Report
.recommendation (displayed)
.codex_rec
.actions
```
---
## Reference Documentation
- **Detection Algorithm:** `lib/patterns/detection-algorithm.md` (shared)
- **Rubric Framework:** `lib/patterns/rubric-framework.md`
- **Platform Rules:** `lib/patterns/platforms/claude-code.md`
- **Platform Rules:** `lib/patterns/platforms/cursor.md`
- **Platform Rules:** `lib/patterns/platforms/gemini-cli.md`
- **Platform Rules:** `lib/patterns/platforms/opencode.md`
- **Platform Rules:** `lib/patterns/platforms/copilot-cli.md`
- **Platform Rules:** `lib/patterns/platforms/codex.md`
- **Injection Checks:** `lib/patterns/injection-checks.md`
---
## Related Skills
- **Uplift plugin:** `skills/uplifting-a-plugin/SKILL.md`Skill
uplifting-a-plugin
Transform any plugin into a fully portable plugin. Any supported plugin platform can act as the starting point.
# Uplifting a Plugin to Multi-Platform Portability
Transform any plugin into a fully portable plugin. No platform is assumed.
**Inputs:**
- `plugin_path` (string, required) — Path to the plugin root directory.
- `platforms` (string, optional) — Comma-separated list of target platforms. If omitted, presents an interactive checklist. Valid: claude-code, cursor, gemini-cli, opencode, copilot-cli, codex, all.
**Output:** Summary of created, skipped, and flagged files.
> **Detection Algorithm:** `lib/patterns/detection-algorithm.md`
> **Manifest Schemas:** `lib/patterns/manifest-generation.md`
> **Manifest Templates:** `lib/templates/manifests/`
> **Context File Templates:** `lib/templates/context-files/`
> **Hook Merging:** `lib/patterns/hook-merging.md`
> **Bootstrapping:** `lib/patterns/bootstrapping.md`
> **Platform References:** `lib/references/copilot-tools.md`, `codex-tools.md`, `gemini-tools.md`
> **Hook Templates:** `lib/templates/hooks/session-start.sh`, `run-hook.cmd`
> **Install Doc Templates:** `lib/templates/install-docs/`
## Overview
| Phase | Description |
| ----- | ----------- |
| **Phase 1: Detect** | Scan metadata, elect canonical, build model, classify shape |
| **Phase 2: Inventory** | Discover assets, init tracking, check conflicts |
| **Phase 3: Recommend** | Recommend uplift target, confirm with user, select target platforms |
| **Phase 4: Generate** | Write missing manifests, context files, sidecars per platform |
| **Phase 5: Port** | Adapt hooks across platforms |
| **Phase 6: Document** | Generate install docs per platform in target repo |
| **Phase 7: Bootstrap** | Opt-in session-start injection |
| **Phase 8: Report** | Summary of created, skipped, flagged files |
**Minimum starting state:** At least one `skills/*/SKILL.md` with `name` + `description`
frontmatter, or any platform manifest file.
**Idempotent:** Running twice on the same repo produces no diff on the second run.
## 1. Phase 1: Detect
Run the shared detection algorithm. See `lib/patterns/detection-algorithm.md` for full detail.
### 1.1 Scan and Infer
```pseudocode
DETECT(plugin_path):
computed.sources = scan_metadata_sources(plugin_path)
IF len(computed.sources) == 0:
DISPLAY "No recognisable plugin signals found in {plugin_path}."
DISPLAY "Provide at least one platform manifest or one skills/*/SKILL.md"
DISPLAY "with name and description frontmatter."
EXIT
computed.canonical = elect_canonical(computed.sources)
computed.metadata = build_metadata_model(computed.sources)
computed.shape = classify_shape(computed.sources)
print_inference_summary(computed.metadata, computed.canonical)
```
## 2. Phase 2: Inventory
### 2.1 Discover Assets
```pseudocode
INVENTORY(plugin_path):
raw_skills = Glob(plugin_path + "/skills/*/SKILL.md")
computed.skills = [
{ path: s, dir: dirname(s), name: basename(dirname(s)) }
FOR s IN raw_skills
]
computed.commands = Glob(plugin_path + "/commands/*.md")
computed.agents = Glob(plugin_path + "/agents/*.md")
computed.existing_hooks = read_json_if_exists(plugin_path + "/hooks/hooks.json")
computed.created = [] # list of { path, platform }
computed.skipped = [] # list of { path, platform }
computed.flagged = [] # list of strings
```
### 2.2 Check Conflicts
```pseudocode
CHECK_CONFLICTS(computed):
conflict_checks = [
{ path: ".claude-plugin/plugin.json", platform: "claude-code" },
{ path: ".claude-plugin/marketplace.json", platform: "claude-code" },
{ path: ".cursor-plugin/plugin.json", platform: "cursor" },
{ path: ".codex-plugin/plugin.json", platform: "codex" },
{ path: ".agents/plugins/marketplace.json", platform: "codex" },
{ path: "gemini-extension.json", platform: "gemini-cli" },
{ path: "GEMINI.md", platform: "gemini-cli" },
{ path: "AGENTS.md", platform: "cross" },
{ path: "CLAUDE.md", platform: "claude-code" },
{ path: "package.json", platform: "opencode" },
{ path: ".opencode/plugins/" + computed.metadata.name + ".js", platform: "opencode" },
{ path: ".github/copilot-instructions.md", platform: "copilot-cli" },
{ path: "hooks/hooks-cursor.json", platform: "cursor" },
]
FOR check IN conflict_checks:
IF file_exists(check.path):
computed.skipped.append({ path: check.path, platform: check.platform })
```
## 3. Phase 3: Recommend
Interactive uplift target recommendation and platform selection. Uses `computed.shape`
from Phase 1 to derive a recommendation, then asks the user to confirm and select
target platforms.
### 3.1 Recommend and Confirm Uplift Target
```pseudocode
RECOMMEND_AND_CONFIRM(computed):
# Derive recommendation from shape
IF computed.shape == "bare-skill-repo":
IF len(computed.skills) <= 3:
recommended = "skill-first"
rationale = "This repo has " + len(computed.skills) + " skill(s) and no platform manifests. Skill-first generates sidecars and context files without full plugin packaging."
ELSE:
recommended = "full-portable-plugin"
rationale = "This repo has " + len(computed.skills) + " skills. Full plugin packaging gives each platform a native manifest for better discoverability."
ELIF computed.shape == "single-platform-plugin":
recommended = "full-portable-plugin"
rationale = "This repo already has one platform manifest. Full plugin packaging adds the remaining platforms."
ELIF computed.shape == "multi-platform-source":
recommended = "full-portable-plugin"
rationale = "This repo already targets multiple platforms. Full plugin packaging fills the remaining gaps."
ELIF computed.shape == "curated-distribution":
recommended = "curated-note-only"
rationale = "This repo is a marketplace distribution without upstream skills. Only install documentation and notes will be generated."
ELSE:
recommended = "full-portable-plugin"
rationale = "Repo shape could not be classified. Defaulting to full plugin packaging."
# If platforms input was provided, auto-confirm
IF inputs.platforms IS PROVIDED:
computed.uplift_target = recommended
RETURN
# Present to user
DISPLAY "## Uplift Target"
DISPLAY "Shape: " + computed.shape
DISPLAY "Recommendation: **" + recommended + "**"
DISPLAY rationale
DISPLAY ""
DISPLAY "Options:"
DISPLAY " 1. skill-first — sidecars, context files, AGENTS.md only (no platform manifests)"
DISPLAY " 2. full-portable-plugin — all platform manifests + context files + sidecars + install docs"
DISPLAY " 3. curated-note-only — install notes only"
response = ASK "Accept recommendation (" + recommended + "), or choose 1/2/3?"
IF response confirms recommendation:
computed.uplift_target = recommended
ELSE:
computed.uplift_target = parse_choice(response)
```
### 3.2 Select Target Platforms
```pseudocode
SELECT_PLATFORMS(computed):
all_platforms = ["claude-code", "cursor", "gemini-cli", "opencode", "copilot-cli", "codex"]
# If platforms input was provided, use it directly
IF inputs.platforms IS PROVIDED:
IF inputs.platforms == "all":
computed.target_platforms = all_platforms
ELSE:
computed.target_platforms = parse_csv(inputs.platforms)
validate_platform_names(computed.target_platforms)
RETURN
# Pre-select based on uplift target and existing state
IF computed.uplift_target == "skill-first":
preselected = all_platforms
ELIF computed.uplift_target == "curated-note-only":
preselected = [p FOR p IN all_platforms
IF any(s.platform == p FOR s IN computed.skipped)]
IF len(preselected) == 0:
preselected = all_platforms
ELSE:
preselected = all_platforms
# Present checklist
DISPLAY "## Target Platforms"
DISPLAY ""
FOR p IN all_platforms:
marker = "[x]" IF p IN preselected ELSE "[ ]"
existing = " (manifest exists)" IF any(s.platform == p FOR s IN computed.skipped) ELSE ""
DISPLAY " " + marker + " " + p + existing
DISPLAY ""
response = ASK "Confirm platforms, or list the ones you want (e.g. 'claude-code, cursor, gemini-cli')?"
IF response confirms:
computed.target_platforms = preselected
ELSE:
computed.target_platforms = parse_platform_list(response)
# Validate: at least one platform required
IF len(computed.target_platforms) == 0:
DISPLAY "At least one platform must be selected."
GOTO SELECT_PLATFORMS
```
### 3.3 Derive Codex Path
```pseudocode
DERIVE_CODEX_PATH(computed):
IF "codex" NOT IN computed.target_platforms:
computed.codex_rec = None
RETURN
IF computed.uplift_target == "skill-first":
computed.codex_rec = "native-skill-discovery"
ELIF computed.uplift_target == "curated-note-only":
computed.codex_rec = "curated-package-note"
ELSE:
computed.codex_rec = "native-plugin-packaging"
```
### 3.4 Early Exit for Curated-Note-Only
```pseudocode
IF computed.uplift_target == "curated-note-only":
SKIP Phase 4 (Generate)
SKIP Phase 5 (Port)
RUN Phase 6 (Document) — install docs only, for selected platforms
SKIP Phase 7 (Bootstrap)
RUN Phase 8 (Report)
RETURN
```
## 4. Phase 4: Generate
### 4.1 Write Platform Manifests
Table-driven generation. See `lib/patterns/manifest-generation.md` for all schemas.
```pseudocode
GENERATE_MANIFESTS(computed):
manifests = [
{ target: ".claude-plugin/plugin.json", platform: "claude-code", schema: "claude-plugin" },
{ target: ".claude-plugin/marketplace.json", platform: "claude-code", schema: "claude-marketplace" },
{ target: "CLAUDE.md", platform: "claude-code", schema: "claude-context" },
{ target: ".cursor-plugin/plugin.json", platform: "cursor", schema: "cursor-plugin" },
{ target: "gemini-extension.json", platform: "gemini-cli", schema: "gemini-extension" },
{ target: "GEMINI.md", platform: "gemini-cli", schema: "gemini-context" },
{ target: "package.json", platform: "opencode", schema: "opencode-package" },
{ target: ".opencode/plugins/{{name}}.js", platform: "opencode", schema: "opencode-shim" },
{ target: ".codex-plugin/plugin.json", platform: "codex", schema: "codex-plugin",
condition: "computed.codex_rec == 'native-plugin-packaging'" },
{ target: ".agents/plugins/marketplace.json", platform: "codex", schema: "codex-marketplace",
condition: "computed.codex_rec == 'native-plugin-packaging'" },
{ target: "AGENTS.md", platform: "cross", schema: "agents-context" },
{ target: ".github/copilot-instructions.md", platform: "copilot-cli", schema: "copilot-instructions" },
]
FOR manifest IN manifests:
# Skip if platform not targeted (cross-platform always included)
IF manifest.platform != "cross" AND manifest.platform NOT IN computed.target_platforms:
CONTINUE
IF manifest.condition AND NOT eval(manifest.condition):
CONTINUE
resolved = substitute(manifest.target, computed.metadata)
IF any(s.path == resolved FOR s IN computed.skipped):
CONTINUE
# Skill-first: skip platform manifests, only generate context files
IF computed.uplift_target == "skill-first" AND is_manifest(manifest.schema):
CONTINUE
template_path = schema_to_template_path(manifest.schema)
mode = schema_to_mode(manifest.schema)
template = Read(template_path)
IF mode == "plain":
content = substitute(template, computed.metadata)
ELIF mode == "conditional":
content = render_with_conditionals(template, computed.metadata, computed)
ELIF mode == "builder":
content = render_with_builder(template, computed.metadata, computed)
Write(resolved, content)
computed.created.append({ path: resolved, platform: manifest.platform })
```
The `is_manifest()` predicate classifies schemas as packaging vs context:
```pseudocode
MANIFEST_SCHEMAS = [
"claude-plugin", "claude-marketplace", "cursor-plugin",
"gemini-extension", "opencode-package", "opencode-shim", "codex-plugin",
"codex-marketplace"
]
CONTEXT_SCHEMAS = [
"claude-context", "gemini-context", "agents-context", "copilot-instructions"
]
FUNCTION is_manifest(schema):
RETURN schema IN MANIFEST_SCHEMAS
```
Under `skill-first`, only context schemas are generated (plus sidecars). Under `full-portable-plugin`, both manifest and context schemas are generated.
### 4.2 Seed Tool-Mapping Sidecars
```pseudocode
GENERATE_SIDECARS(computed):
sidecar_platform_map = {
"copilot-tools.md": "copilot-cli",
"codex-tools.md": "codex",
"gemini-tools.md": "gemini-cli",
}
FOR skill IN computed.skills:
FOR sidecar, platform IN sidecar_platform_map:
IF platform NOT IN computed.target_platforms:
CONTINUE
target = skill.dir + "/references/" + sidecar
IF NOT file_exists(target):
source = Read("lib/references/" + sidecar)
Write(target, source)
computed.created.append({ path: target, platform: platform })
```
### 4.3 Validate npx Skills Frontmatter
```pseudocode
VALIDATE_FRONTMATTER(computed):
FOR skill IN computed.skills:
frontmatter = parse_yaml_frontmatter(Read(skill.path))
IF NOT frontmatter.name OR NOT frontmatter.description:
computed.flagged.append(
skill.path + " — missing frontmatter field(s). Add name: and description: in YAML frontmatter."
)
```
Do NOT auto-write — frontmatter descriptions require human authorship.
## 5. Phase 5: Port
Adapt hooks from any source platform to all target platforms.
See `lib/patterns/hook-merging.md` for event mapping and merge logic.
Hook porting is filtered by `computed.target_platforms`. Each subsection only
runs if its target platform is selected.
### 5.1 Claude Code → Cursor Hooks
```pseudocode
PORT_CURSOR_HOOKS(computed):
IF "cursor" NOT IN computed.target_platforms:
RETURN
IF any(s.path == "hooks/hooks-cursor.json" FOR s IN computed.skipped):
RETURN
IF computed.existing_hooks:
generate_cursor_hooks(computed.existing_hooks)
ELSE:
Write("hooks/hooks-cursor.json", '{ "version": 1, "hooks": {} }')
computed.created.append({ path: "hooks/hooks-cursor.json", platform: "cursor" })
```
### 5.2 Claude Code → Copilot Hooks
Copilot uses separate `bash` / `powershell` fields, no `matcher`, stored under `.github/hooks/`.
```pseudocode
PORT_COPILOT_HOOKS(computed):
IF "copilot-cli" NOT IN computed.target_platforms:
RETURN
IF NOT computed.existing_hooks:
RETURN
copilot_hooks = adapt_hooks_copilot(computed.existing_hooks) # see hook-merging.md
FOR hook IN copilot_hooks:
target = ".github/hooks/" + hook.event + ".sh"
IF NOT file_exists(target):
Write(target, hook.bash)
computed.created.append({ path: target, platform: "copilot-cli" })
win_target = ".github/hooks/" + hook.event + ".ps1"
IF NOT file_exists(win_target):
Write(win_target, hook.powershell)
computed.created.append({ path: win_target, platform: "copilot-cli" })
```
### 5.3 Gemini Hook Guidance
Gemini CLI hooks are configured interactively by users via `GEMINI.md` instructions —
they cannot be written as files. Capture guidance text to include in install docs.
```pseudocode
GEMINI_HOOK_GUIDANCE(computed):
IF "gemini-cli" NOT IN computed.target_platforms:
RETURN
IF computed.existing_hooks:
computed.gemini_hook_text = render_gemini_hook_instructions(computed.existing_hooks)
ELSE:
computed.gemini_hook_text = NULL
```
### 5.4 Windows Support
```pseudocode
PORT_WINDOWS_HOOKS(computed):
IF file_exists("lib/templates/hooks/run-hook.cmd"):
source = Read("lib/templates/hooks/run-hook.cmd")
Write("hooks/run-hook.cmd", source)
computed.created.append({ path: "hooks/run-hook.cmd", platform: "cross" })
```
## 6. Phase 6: Document
Generate install documentation for every platform that received artifacts.
See `lib/templates/install-docs/` for section templates.
### 6.1 Determine Platforms With Artifacts
```pseudocode
DETERMINE_PLATFORMS(computed):
platforms_with_artifacts = computed.target_platforms
```
### 6.2 Render Per-Platform Install Sections
```pseudocode
RENDER_INSTALL_SECTIONS(computed, platforms_with_artifacts):
sections = {}
FOR platform IN platforms_with_artifacts:
template = Read("lib/templates/install-docs/" + platform + ".md")
sections[platform] = render(template, computed.metadata)
IF computed.gemini_hook_text:
sections["gemini-cli"] += "\n\n### Hook Setup\n\n" + computed.gemini_hook_text
RETURN sections
```
### 6.3 Write Install Docs
```pseudocode
WRITE_INSTALL_DOCS(computed, sections, platforms_with_artifacts):
# Whole-repo note: only include when plugin has shared assets that require
# whole-repo install (hooks, session-start bootstrapping, root context files,
# or platform manifests). Bare skill repos without these can use npx skills.
has_shared_assets = (
computed.existing_hooks
OR file_exists("skills/using-" + computed.metadata.name + "/SKILL.md")
OR any(file_exists(p) FOR p IN ["CLAUDE.md", "AGENTS.md", "GEMINI.md"])
OR computed.uplift_target == "full-portable-plugin"
)
IF has_shared_assets:
whole_repo_note = render(Read("lib/templates/install-docs/whole-repo-note.md"), computed.metadata)
ELSE:
whole_repo_note = ""
fresh_install = ""
adding_platform = ""
FOR platform IN platforms_with_artifacts:
fresh_install += sections[platform] + "\n\n"
adding_tmpl = read_if_exists("lib/templates/install-docs/adding-platform/" + platform + ".md")
IF adding_tmpl:
adding_platform += render(adding_tmpl, computed.metadata) + "\n\n"
content = "# Installation\n\n"
IF whole_repo_note:
content += whole_repo_note + "\n\n"
content += "## Fresh Install\n\n" + fresh_install
content += "## Adding Another Platform\n\n"
content += "Already have the repo cloned for one platform? Add others by pointing them at the same checkout.\n\n"
content += adding_platform
Write("INSTALL.md", content)
computed.created.append({ path: "INSTALL.md", platform: "cross" })
# Platform-specific pointers (not full docs)
IF "copilot-cli" IN platforms_with_artifacts:
Write(".github/INSTALL.md", "See [INSTALL.md](../INSTALL.md) for installation instructions.\n")
computed.created.append({ path: ".github/INSTALL.md", platform: "copilot-cli" })
IF "codex" IN platforms_with_artifacts:
Write(".codex/INSTALL.md", "See [INSTALL.md](../INSTALL.md) for installation instructions.\n")
computed.created.append({ path: ".codex/INSTALL.md", platform: "codex" })
# Flag missing Installation and Publishing sections in README
IF file_exists("README.md"):
readme = Read("README.md")
IF "## Installation" NOT IN readme AND "## Install" NOT IN readme:
computed.flagged.append(
"README.md — no Installation section found. Add install instructions or link to INSTALL.md."
)
IF "PUBLISHING.md" NOT IN readme:
computed.flagged.append(
"README.md — no link to PUBLISHING.md. Add a link so plugin authors can find publishing guidance."
)
```
### 6.4 Write Publishing Docs
```pseudocode
WRITE_PUBLISHING_DOCS(computed, platforms_with_artifacts):
header = render(Read("lib/templates/install-docs/publishing.md"), computed.metadata)
sections = ""
FOR platform IN platforms_with_artifacts:
template = read_if_exists("lib/templates/install-docs/publishing/" + platform + ".md")
IF template:
sections += render(template, computed.metadata) + "\n\n"
IF sections:
content = header + "\n\n" + sections
Write("PUBLISHING.md", content)
computed.created.append({ path: "PUBLISHING.md", platform: "cross" })
```
## 7. Phase 7: Bootstrap (opt-in)
Session-start injection. See `lib/patterns/bootstrapping.md` for full generation logic.
### 7.1 Prompt and Execute
```pseudocode
BOOTSTRAP(computed):
IF file_exists("skills/using-" + computed.metadata.name + "/SKILL.md"):
computed.bootstrap_status = "already-configured"
RETURN
response = ASK "Generate session-start bootstrapping hooks? (y/n)"
IF response == "no":
computed.bootstrap_status = "declined"
RETURN
generate_using_skill(computed)
generate_using_sidecars(computed) # filtered by target_platforms (same as Phase 4.2)
generate_session_start(computed)
generate_run_hook_cmd(computed)
# Hook merging gated on platform targeting
IF "claude-code" IN computed.target_platforms:
merge_session_start_hooks_claude(computed)
IF "cursor" IN computed.target_platforms:
merge_session_start_hooks_cursor(computed)
# Platform-specific enhancements
IF "opencode" IN computed.target_platforms:
enhance_opencode_plugin(computed)
IF "gemini-cli" IN computed.target_platforms:
update_gemini_md(computed)
computed.bootstrap_status = "configured"
```
## 8. Phase 8: Report
Emit the final uplift report. See `lib/patterns/report-template.md` for the full report format and state flow diagram.
## Related Skills
- **Assess portability:** `skills/assessing-plugin-portability/SKILL.md`Skill
using-skill-portability
Use when starting a session with the skill-portability plugin. Session-start bootstrapping that lists available skills and platform-specific invocation instructions.
# Using Skill Portability
This plugin provides the following skills:
| Skill | Description |
| ----- | ----------- |
| `uplifting-a-plugin` | Add multi-platform portability to any plugin. Accepts any starting state — Claude, Cursor, Gemini, OpenCode, Copilot, Codex, or bare SKILL.md files. Detects what exists, infers canonical metadata, generates every missing platform artifact, ports hooks, produces install documentation, and optionally configures session-start bootstrapping. |
| `assessing-plugin-portability` | Assess a plugin for multi-platform portability. Classifies repo shape, scores readiness per platform using a 7-category rubric, detects structural blockers, and recommends an uplift target. Read-only — makes no changes. |
## How to Invoke Skills
**Claude Code / Cursor:** Use the `Skill` tool with the skill name.
**Copilot CLI:** Use the `skill` tool with the skill name.
**Gemini CLI:** Use the `activate_skill` tool with the skill name.
**Codex / Other:** Skills are auto-discovered. Follow the SKILL.md instructions directly.
## Tool Name Mapping
Skills use Claude Code tool names. See `lib/references/` for platform-specific equivalents.规则
hooks
Event hooks configuration
{
"hooks": {
"SessionStart": [
{
"command": "hooks/run-hook.cmd session-start",
"matcher": "startup|clear|compact"
}
]
}
}