fix: register aictrl marketplace so Claude Code can resolve plugin enablement (#18)#19
Conversation
…ablement Closes #18 Before this commit installClaudePlugin wrote the plugin to ~/.claude/plugins/cache/<plugin>@aictrl/ and flipped settings.enabledPlugins["<plugin>@aictrl"] = true, but never told Claude Code that the "aictrl" marketplace existed. The loader then reported "Plugin not found in marketplace aictrl" on every launch because there was no known_marketplaces.json entry, no marketplace manifest, and no installed_plugins.json record to resolve. Switch to the canonical Claude Code plugin layout: ~/.claude/plugins/marketplaces/aictrl/ .claude-plugin/marketplace.json ← declares plugin plugins/aictrl-<orgSlug>/ ← plugin lives here .claude-plugin/plugin.json .mcp.json hooks/... skills/... ~/.claude/plugins/known_marketplaces.json ← registers aictrl ~/.claude/plugins/installed_plugins.json ← records install The marketplace is registered as source=local pointing at the local marketplace dir. The enabledPlugins key shape ("<plugin>@aictrl") is unchanged, so existing settings.json entries continue to work after upgrade. Adds a cleanup step that removes the orphan pre-fix cache directory (~/.claude/plugins/cache/<plugin>@aictrl/) on re-install so upgraders do not leave stale state behind. Updates the slash-command telemetry hook to search both cache/ and marketplaces/ for SKILL.md files now that plugins live in either location. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Now I have all the patches. Let me build the manifests and dispatch subagents. Manifests:
Deduplication:
1 MAJOR · 5 MINOR · 1 NIT MAJOR
The legacy-cleanup block constructs const legacyCacheDir = join(pluginsRoot, 'cache', pluginDirName);
if (existsSync(legacyCacheDir)) {
await rm(legacyCacheDir, { recursive: true, force: true });Suggested fix: Validate MINOR
Suggested fix: Remove the
manifest = { ...manifest, ...parsed };
if (!Array.isArray(manifest.plugins)) manifest.plugins = [];Suggested fix: Guard with
Same if (parsed && typeof parsed === 'object') {
data = parsed;Suggested fix: Change the guard to
Suggested fix: Merge instead of replace: find the 'user'-scope entry in the existing array, update it in place, and preserve sibling entries from other scopes.
Suggested fix: Remove the NIT
The |
- [MAJOR] validate orgSlug at entry to installClaudePlugin to reject path-traversal sequences before any filesystem write or recursive rm - [MINOR] drop redundant existsSync guard (rm with force:true already no-ops on missing path) — also removes the sync fs import [NIT] - [MINOR] reject array-shaped JSON when reading marketplace.json, known_marketplaces.json, and installed_plugins.json so a corrupt file does not pollute the rewritten output or silently lose entries - [MINOR] preserve non-user-scope entries in installed_plugins.json across re-install — partition by scope and only replace user row - [MINOR] remove unused CLAUDE_PLUGINS_CACHE export from src/config.ts 5 new regression tests in test/writers/claude.test.ts; 108/108 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review response — PR #19All 7 findings verified TRUE and all fixed in this PR (no defers, no false positives). Issues addressed (pushed to this PR)
5 regression tests added in
Suite: 108/108 pass (was 103). Review claims verified false (no change needed)None — all 7 findings were verified true and worth fixing. Not addressed hereNone — every finding is in scope and applied to this branch. |
…tamp assertion The per-file tests for marketplace.json, known_marketplaces.json, and installed_plugins.json each validate one file in isolation. None of them catches the failure mode where the three files agree file-by-file but disagree with each other (typo in marketplace name, wrong relative source path, mismatched pluginId vs pluginDirName) — which is the exact bug shape #18 was about, just transposed. Adds a single test that walks the full handshake Claude Code performs at load time: settings.enabledPlugins → known_marketplaces.json → marketplace.json plugin spec → installed_plugins.json installPath → plugin.json name. If any link drifts, this test fails. Also tightens the installedAt-preservation test: assert lastUpdated >= installedAt as parsed dates instead of relying on string inequality, which is brittle on coarse-resolution timers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Closes #18.
@aictrl/pluginwas writing the plugin into~/.claude/plugins/cache/<plugin>@aictrl/and flippingsettings.enabledPlugins["<plugin>@aictrl"] = true, but it never registered theaictrlmarketplace with Claude Code. The loader couldn't resolve the enablement entry and printedPlugin "<plugin>" not found in marketplace "aictrl"on every session.Root cause
Claude Code needs three index files to resolve a
<plugin>@<marketplace>enablement:~/.claude/plugins/known_marketplaces.json— registers the marketplace~/.claude/plugins/marketplaces/<name>/.claude-plugin/marketplace.json— declares the plugin~/.claude/plugins/installed_plugins.json— records where the install livesThe old installer wrote (0) the on-disk plugin and (4) the
enabledPluginstoggle, but skipped (1)–(3). That orphaned every install.Fix
Switch to the canonical Claude Code layout. Plugin files now live at:
The installer also merges entries into
known_marketplaces.json(source=local) andinstalled_plugins.json. TheenabledPluginskey shape (<plugin>@aictrl) is unchanged, so settings written by older versions still resolve after upgrade.Cleanup: on re-install we delete the orphan
~/.claude/plugins/cache/<plugin>@aictrl/directory left by pre-fix versions so upgraders don't end up with stale state.Hook update:
slash-command-telemetry.shnow searches both~/.claude/plugins/cacheand~/.claude/plugins/marketplacesforSKILL.mdfiles, since canonical plugins live in the second location.Repro (from #18)
npx @aictrl/pluginfor any org.Plugin "aictrl-<orgSlug>" not found in marketplace "aictrl"every session.Test plan
Unit tests added in
test/writers/claude.test.ts(6 new tests, 103 total — up from 97):writes marketplace manifest declaring the pluginregisters the marketplace in known_marketplaces.jsonpreserves existing marketplaces when registering aictrlrecords the install in installed_plugins.jsonpreserves installedAt across re-installs but updates lastUpdatedcleans up legacy cache/<plugin>@aictrl/ directory from older installsPlus the existing 8
installClaudePlugintests were updated to the new on-disk paths, and one assertion intest/hooks/claude-slash.test.tswas widened to match the broaderfindroot.npm test— 103/103 passnpm run build— clean tscnpx @aictrl/pluginagainst a real org, open Claude Code, confirm no banner🤖 Generated with Claude Code