Skip to content

feat(skills): publish registry skills to all hosts#110

Merged
BlackHole1 merged 2 commits into
mainfrom
fix-skills-install
Apr 28, 2026
Merged

feat(skills): publish registry skills to all hosts#110
BlackHole1 merged 2 commits into
mainfrom
fix-skills-install

Conversation

@BlackHole1
Copy link
Copy Markdown
Member

Registry skills now follow the same host-aware installation model as bundled skills. This keeps Codex, Claude Code, and OpenClaw targets consistent during install, update, and uninstall, while rejecting unmanaged same-name directories before publication.

Registry skills now follow the same host-aware installation model as
bundled skills. This keeps Codex, Claude Code, and OpenClaw targets
consistent during install, update, and uninstall, while rejecting
unmanaged same-name directories before publication.

Signed-off-by: Kevin Cui <bh@bugs.cc>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Summary by CodeRabbit

  • New Features

    • Skills management now operates across multiple local AI agent hosts; install/update/uninstall act per supported host with host-specific install modes (symlink-or-copy vs copy) and per-host success reporting.
  • Bug Fixes

    • Prevent overwriting non-managed targets (missing/invalid metadata); tighten ownership detection to require parseable metadata with a version; improved conflict detection.
  • Documentation

    • Revised English/Chinese command docs and help text to reflect multi-host behavior and updated command semantics.
  • Tests

    • Expanded tests cover multi-host install/update/uninstall and metadata-error cases.
  • Chores

    • Updated i18n strings for generalized host terminology.

Walkthrough

This PR converts skills command handling from a Codex-focused model to a multi-host "AI Agent Skills" model (Codex, Claude Code, OpenClaw). It adds a managed-skill-hosts abstraction and exports ManagedSkillHost/ManagedSkillHostInstallation, changes install/update/uninstall flows to operate across resolved hosts (per-host publication, validation, and reporting), updates publication metadata to carry per-host results and publication modes, tightens ownership detection to parseable .oo-metadata.json with a non-empty version, and updates docs, tests, and i18n to reflect multi-host behavior and conflict rules for unmanaged targets.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as CLI Command
    participant Resolver as Host Resolver
    participant Registry as Registry/Package
    participant Validator as Validator
    participant Publisher as Publisher
    participant FS as File System

    User->>CLI: oo skills install <skill>
    CLI->>Resolver: resolveAvailableManagedSkillHosts(env)
    Resolver-->>CLI: list of ManagedSkillHosts

    CLI->>Registry: fetch package metadata & tarball
    Registry-->>CLI: skill metadata & archive

    loop for each ManagedSkillHost
        CLI->>Validator: validateRegistrySkillPublicationTargets(hostInstallations)
        alt target exists & lacks managed metadata
            Validator-->>CLI: throw CliUserError (name conflict)
        else
            Validator-->>CLI: ok to publish
        end
    end

    loop for each ManagedSkillHost
        CLI->>Publisher: publishPreparedRegistrySkillPublication(hostInstallations)
        alt agentName == "openclaw"
            Publisher->>FS: copy files to installedSkillDirectoryPath
        else
            Publisher->>FS: symlink-or-copy to installedSkillDirectoryPath
        end
        Publisher->>FS: write .oo-metadata.json
        Publisher-->>CLI: RegistrySkillPublicationResult (agentName, status)
    end

    CLI->>User: report per-host publication results
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title follows the required format with type and scope, accurately summarizing the main change of extending registry skill publication to multiple hosts.
Description check ✅ Passed The description directly relates to the changeset, explaining the host-aware installation model for registry skills and unmanaged directory rejection.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix-skills-install

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docs/commands.zh-CN.md (1)

285-316: ⚠️ Potential issue | 🟡 Minor

Keep this reference at the CLI contract level.

These new bullets document canonical storage layout and symlink/copy publication mechanics. That is internal implementation detail rather than stable user-visible behavior, so it makes the command reference harder to keep accurate and accidentally turns internals into compatibility promises.

As per coding guidelines, docs/commands*.md should describe only user-facing CLI contract (purpose, arguments, options, output shapes, observable behavior), not internal implementation details.

Also applies to: 337-360

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/commands.zh-CN.md` around lines 285 - 316, The new bullets reveal
internal implementation (e.g., "canonical 目录", explicit paths like
`<config-dir>/skills/bundled/<agent>/<skill-id>`, "安装方式" describing symlink vs
copy, and hidden file `.oo-metadata.json`) which shouldn't appear in CLI
contract docs; remove or rewrite those bullets so docs only state user-facing,
observable behavior (what the command does, required flags like `--skill`/`--all
-y`, that auth is sent, upgrade/rollback effects) without internal layout/path
or implementation details, and move any necessary implementation specifics to
internal/developer docs; apply the same edits to the corresponding section
referenced (lines 337-360) so both places only present stable CLI contract
information.
src/application/commands/skills/registry-skill-install.ts (1)

386-449: ⚠️ Potential issue | 🟠 Major

Don't route unmanaged targets through the overwrite-confirmation flow.

readRegistrySkillInstallStatus() collapses “directory exists but is not oo-managed” into the generic conflict state, so explicit installs either prompt for overwrite or fail with errors.skills.install.confirmationRequired in non-TTY mode. validateRegistrySkillPublicationTargets() later rejects those same targets anyway, so this branch can never lead to a successful install. Unmanaged host targets should surface errors.skills.nameConflict directly here.

Also applies to: 488-525

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/application/commands/skills/registry-skill-install.ts` around lines 386 -
449, The conflict branch in filterConfirmedSkillNames should not route unmanaged
targets through the overwrite confirmation flow; instead detect unmanaged
targets (using the existing validateRegistrySkillPublicationTargets helper or
equivalent validation logic) immediately after readRegistrySkillInstallStatus
returns "conflict" and throw a CliUserError with "errors.skills.nameConflict"
(including the name) for unmanaged targets; only if the target is a managed
conflict proceed with interactive confirmation as currently implemented. Apply
the same change to the analogous block later (lines noted in the review).
🧹 Nitpick comments (2)
src/application/commands/skills/update.test.ts (1)

110-207: Add OpenClaw coverage to the new multi-host update regression.

This test now proves the Codex + Claude path, but skills update also republishes to OpenClaw. Since OpenClaw uses a different publication mode from the symlink-first hosts, a host-specific update regression can still pass this suite.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/application/commands/skills/update.test.ts` around lines 110 - 207, The
test only covers Codex and Claude hosts but omits OpenClaw, so add OpenClaw to
the setup and assertions: create openClaw home via
resolveOpenClawHomeDirectory(sandbox.env), set
openclawInstalledSkillDirectoryPath = join(openClawHomeDirectory, "skills",
"chatgpt"), mkdir that directory in the same Promise.all that makes codex/claude
dirs, write an initial SKILL.md and metadata (renderSkillMetadataJson version
"0.0.3") to that OpenClaw path before running sandbox.run, then update the
expected stdout to include the OpenClaw update message (similar to the
Codex/Claude lines) and add assertions that
resolveManagedSkillMetadataFilePath(openclawInstalledSkillDirectoryPath) equals
the "0.0.4" metadata and that SKILL.md contents at
join(openclawInstalledSkillDirectoryPath, "SKILL.md") contain "# ChatGPT fresh"
(mirror the checks used for claude/codex).
src/application/commands/skills/registry-skill-publication.ts (1)

109-142: Consider reporting all conflicting targets, not just the first one.

The validation function uses find() at line 130-132, which stops at the first unmanaged target. If multiple hosts have conflicting unmanaged directories, the user will only see one error at a time. This could lead to a frustrating fix-and-retry cycle.

Consider collecting all conflicts and reporting them together so users can resolve all issues in one pass.

♻️ Proposed fix to report all conflicts
-    const unmanagedTarget = targetStates.find(
-        target => target.installedDirectoryExists && target.metadata === undefined,
+    const unmanagedTargets = targetStates.filter(
+        target => target.installedDirectoryExists && target.metadata === undefined,
     );

-    if (unmanagedTarget === undefined) {
+    if (unmanagedTargets.length === 0) {
         return;
     }

     throw new CliUserError("errors.skills.nameConflict", 1, {
         name: options.skillName,
-        path: unmanagedTarget.installation.installedSkillDirectoryPath,
+        path: unmanagedTargets
+            .map(t => t.installation.installedSkillDirectoryPath)
+            .join(", "),
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/application/commands/skills/registry-skill-publication.ts` around lines
109 - 142, The current validateRegistrySkillPublicationTargets function only
reports the first unmanaged target because it uses find(); change this to
collect all unmanaged targets by filtering targetStates for entries where
installedDirectoryExists is true and metadata is undefined (e.g., const
unmanagedTargets = targetStates.filter(...)), and if unmanagedTargets.length > 0
throw the CliUserError including all conflicting installation paths (map
unmanagedTargets to their installation.installedSkillDirectoryPath) and any
other useful metadata (like names) so the error payload reports all conflicts at
once instead of a single path. Ensure you update the thrown error payload in
validateRegistrySkillPublicationTargets accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/application/commands/skills/update.ts`:
- Around line 271-308: The merge currently treats entries with metadata:
undefined as managed; update mergeManagedSkillInstallationsByName (and the same
logic at the other occurrence) to exclude entries whose metadata is exactly
undefined so only parseable .oo-metadata.json counts as managed, and when
performing the host fallback check (in readKnownManagedSkillInstallations flow /
resolveSelectedManagedSkills) use readManagedSkillMetadata() to re-read/validate
host metadata instead of relying on fileExists() so corrupted installs aren’t
treated as managed.

---

Outside diff comments:
In `@docs/commands.zh-CN.md`:
- Around line 285-316: The new bullets reveal internal implementation (e.g.,
"canonical 目录", explicit paths like
`<config-dir>/skills/bundled/<agent>/<skill-id>`, "安装方式" describing symlink vs
copy, and hidden file `.oo-metadata.json`) which shouldn't appear in CLI
contract docs; remove or rewrite those bullets so docs only state user-facing,
observable behavior (what the command does, required flags like `--skill`/`--all
-y`, that auth is sent, upgrade/rollback effects) without internal layout/path
or implementation details, and move any necessary implementation specifics to
internal/developer docs; apply the same edits to the corresponding section
referenced (lines 337-360) so both places only present stable CLI contract
information.

In `@src/application/commands/skills/registry-skill-install.ts`:
- Around line 386-449: The conflict branch in filterConfirmedSkillNames should
not route unmanaged targets through the overwrite confirmation flow; instead
detect unmanaged targets (using the existing
validateRegistrySkillPublicationTargets helper or equivalent validation logic)
immediately after readRegistrySkillInstallStatus returns "conflict" and throw a
CliUserError with "errors.skills.nameConflict" (including the name) for
unmanaged targets; only if the target is a managed conflict proceed with
interactive confirmation as currently implemented. Apply the same change to the
analogous block later (lines noted in the review).

---

Nitpick comments:
In `@src/application/commands/skills/registry-skill-publication.ts`:
- Around line 109-142: The current validateRegistrySkillPublicationTargets
function only reports the first unmanaged target because it uses find(); change
this to collect all unmanaged targets by filtering targetStates for entries
where installedDirectoryExists is true and metadata is undefined (e.g., const
unmanagedTargets = targetStates.filter(...)), and if unmanagedTargets.length > 0
throw the CliUserError including all conflicting installation paths (map
unmanagedTargets to their installation.installedSkillDirectoryPath) and any
other useful metadata (like names) so the error payload reports all conflicts at
once instead of a single path. Ensure you update the thrown error payload in
validateRegistrySkillPublicationTargets accordingly.

In `@src/application/commands/skills/update.test.ts`:
- Around line 110-207: The test only covers Codex and Claude hosts but omits
OpenClaw, so add OpenClaw to the setup and assertions: create openClaw home via
resolveOpenClawHomeDirectory(sandbox.env), set
openclawInstalledSkillDirectoryPath = join(openClawHomeDirectory, "skills",
"chatgpt"), mkdir that directory in the same Promise.all that makes codex/claude
dirs, write an initial SKILL.md and metadata (renderSkillMetadataJson version
"0.0.3") to that OpenClaw path before running sandbox.run, then update the
expected stdout to include the OpenClaw update message (similar to the
Codex/Claude lines) and add assertions that
resolveManagedSkillMetadataFilePath(openclawInstalledSkillDirectoryPath) equals
the "0.0.4" metadata and that SKILL.md contents at
join(openclawInstalledSkillDirectoryPath, "SKILL.md") contain "# ChatGPT fresh"
(mirror the checks used for claude/codex).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 837cc0b7-f737-49e1-b92c-baecb1217997

📥 Commits

Reviewing files that changed from the base of the PR and between f0b4b2e and 7e3d5cd.

⛔ Files ignored due to path filters (3)
  • src/application/bootstrap/__snapshots__/run-cli.test.ts.snap is excluded by !**/*.snap
  • src/application/commands/config/__snapshots__/index.cli.test.ts.snap is excluded by !**/*.snap
  • src/application/commands/skills/__snapshots__/index.cli.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (11)
  • docs/commands.md
  • docs/commands.zh-CN.md
  • src/application/commands/skills/index.test.ts
  • src/application/commands/skills/list.ts
  • src/application/commands/skills/managed-skill-hosts.ts
  • src/application/commands/skills/registry-skill-install.ts
  • src/application/commands/skills/registry-skill-publication.ts
  • src/application/commands/skills/shared.ts
  • src/application/commands/skills/update.test.ts
  • src/application/commands/skills/update.ts
  • src/i18n/catalog.ts

Comment thread src/application/commands/skills/update.ts
Treat installed skills whose metadata cannot be parsed or has no managed
version as unmanaged during skills update. This prevents broken local
installations from being selected during all-target updates.

Include host names in missing package metadata errors so explicit update
failures identify every affected host.

Signed-off-by: Kevin Cui <bh@bugs.cc>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/application/commands/skills/update.ts (1)

513-522: Reuse resolved host-installation sets instead of recomputing them.

resolveManagedSkillHostInstallations(...) is recomputed multiple times for the same skill in the same update flow. Cache per-skill host installations once in prepareRegistrySkillGroupUpdate and pass them through to validation/preparation/current checks.

As per coding guidelines "Compute an expression once and store in a variable for reuse, especially inside switch statements and loops (no duplicate computations)".

Also applies to: 539-549, 566-569, 608-611

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/application/commands/skills/update.ts` around lines 513 - 522,
prepareRegistrySkillGroupUpdate is recomputing
resolveManagedSkillHostInstallations(...) for the same skill multiple times;
cache the resolved host-installation set per skill inside
prepareRegistrySkillGroupUpdate and pass that cached value into the downstream
checks (e.g., into isRegistrySkillCurrentInAllHosts and the
validation/preparation/current-check routines) instead of calling
resolveManagedSkillHostInstallations repeatedly. Update signatures or local call
sites so the cached installations map (keyed by skill.name) is used for all
occurrences in the update flow (including the blocks that currently call
resolveManagedSkillHostInstallations for validation, preparation and
current-state checks) to eliminate duplicate computation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/application/commands/skills/update.ts`:
- Around line 317-337: The current merge conflates skills with the same
skill.name but different non-empty metadata.packageName values by merging
hostNames into a single skillsByName entry; change the logic in the update loop
that uses mergeManagedSkillHostNames, existingSkill, skill and skillsByName so
that when both existingSkill.metadata.packageName and skill.metadata.packageName
are defined and are different you do NOT merge them into one entry — instead
create distinct entries (e.g., key by a composite identifier like
`${skill.name}::${packageName}` or otherwise append the packageName to the map
key) so each package-specific skill keeps its own hostNames and metadata;
preserve the existing behavior only when existing packageName is undefined and
incoming packageName is defined as currently implemented.

---

Nitpick comments:
In `@src/application/commands/skills/update.ts`:
- Around line 513-522: prepareRegistrySkillGroupUpdate is recomputing
resolveManagedSkillHostInstallations(...) for the same skill multiple times;
cache the resolved host-installation set per skill inside
prepareRegistrySkillGroupUpdate and pass that cached value into the downstream
checks (e.g., into isRegistrySkillCurrentInAllHosts and the
validation/preparation/current-check routines) instead of calling
resolveManagedSkillHostInstallations repeatedly. Update signatures or local call
sites so the cached installations map (keyed by skill.name) is used for all
occurrences in the update flow (including the blocks that currently call
resolveManagedSkillHostInstallations for validation, preparation and
current-state checks) to eliminate duplicate computation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1270bdd6-6243-4665-acb7-f4faa0b4ff57

📥 Commits

Reviewing files that changed from the base of the PR and between 7e3d5cd and d827c08.

📒 Files selected for processing (5)
  • docs/commands.md
  • docs/commands.zh-CN.md
  • src/application/commands/skills/update.test.ts
  • src/application/commands/skills/update.ts
  • src/i18n/catalog.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/application/commands/skills/update.test.ts
  • docs/commands.md

Comment thread src/application/commands/skills/update.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant