feat(installer): community module browser and custom URL support#2229
feat(installer): community module browser and custom URL support#2229
Conversation
Three-tier module selection: official, community (category drill-down with featured/search), and custom GitHub URL. - Add RegistryClient shared fetch utility - Add CommunityModuleManager with SHA-pinned cloning (refuses install if approved SHA cannot be reached; uses HEAD when no SHA set) - Add CustomModuleManager for arbitrary GitHub repo installation - Extend findModuleSource chain with community and custom fallthrough - Extend manifest to detect community and custom source types - Add Config.customModulesMeta for custom module metadata
🤖 Augment PR SummarySummary: Adds a three-tier module selection/install flow to the installer, expanding beyond official modules. Changes:
Technical Notes: Community index + categories are pulled from the marketplace repo; custom modules are treated as unverified and are pre-cloned to cache for the install pipeline. 🤖 Was this summary useful? React with 👍 or 👎 |
tools/installer/ui.js
Outdated
| const customSelected = await this._addCustomUrlModules(installedModuleIds); | ||
|
|
||
| // Merge all selections | ||
| return [...officialSelected, ...communitySelected, ...customSelected]; |
There was a problem hiding this comment.
tools/installer/ui.js:581 — selectAllModules() concatenates the three selections without de-duplicating module codes, so the same code can appear multiple times and be processed twice downstream.
This could lead to repeated installs/config prompts if a module ID overlaps across tiers.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
tools/installer/ui.js
Outdated
| /** | ||
| * Prompt user to install modules from custom GitHub URLs. | ||
| * @param {Set} installedModuleIds - Currently installed module IDs | ||
| * @returns {Array} Selected custom module objects with metadata |
There was a problem hiding this comment.
tools/installer/ui.js:804 — The JSDoc for _addCustomUrlModules() says it returns “custom module objects with metadata”, but the function returns an array of module code strings.
This mismatch can mislead callers and future maintenance.
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| * Manages community modules from the BMad marketplace registry. | ||
| * Fetches community-index.yaml and categories.yaml from GitHub. | ||
| * Returns empty results when the registry is unreachable. | ||
| * Community modules are pinned to approved tags (not HEAD). |
There was a problem hiding this comment.
tools/installer/modules/community-manager.js:16 — The class doc says community modules are pinned to approved tags “(not HEAD)”, but cloneModule() updates to origin/HEAD whenever approvedSha is not set.
This comment/code mismatch can lead to incorrect assumptions about what gets installed.
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| return path.dirname(rootCandidate); | ||
| } | ||
|
|
||
| return moduleInfo.moduleDefinition ? path.dirname(path.join(cloneDir, moduleInfo.moduleDefinition)) : null; |
There was a problem hiding this comment.
tools/installer/modules/community-manager.js:336 — findModuleSource() returns a directory derived from moduleDefinition even when the configured file doesn’t exist, which can cause later steps to assume a valid module.yaml path.
Returning a non-existent source path here could turn a registry/config error into a harder-to-debug install failure.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| }); | ||
| installSpinner.stop(`Installed dependencies for ${moduleInfo.displayName}`); | ||
| } catch (error) { | ||
| installSpinner.error(`Failed to install dependencies for ${moduleInfo.displayName}`); |
There was a problem hiding this comment.
tools/installer/modules/community-manager.js:280 — If dependency installation fails, the error is logged but the install continues, potentially leaving a cached module in a broken/half-installed state.
Consider whether this should fail the module install step so downstream code doesn’t run against missing deps.
Severity: medium
Other Locations
tools/installer/modules/custom-module-manager.js:172
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| const trimmed = url.trim(); | ||
|
|
||
| // HTTPS format: https://github.com/owner/repo[.git] | ||
| const httpsMatch = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/); |
There was a problem hiding this comment.
tools/installer/modules/custom-module-manager.js:33 — validateGitHubUrl() rejects common GitHub URL variants like a trailing slash (.../owner/repo/) and repo names containing dots, which may block legitimate repos.
This can make the custom URL flow fail unexpectedly for users.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| const customMgr = new CustomModuleManager(); | ||
| const customSource = await customMgr.findModuleSourceByCode(moduleName); | ||
| if (customSource) { | ||
| const customVersion = await this._readMarketplaceVersion(moduleName, moduleSourcePath); |
There was a problem hiding this comment.
tools/installer/core/manifest.js:840 — In the custom-module block, customSource is computed but not used, so _readMarketplaceVersion() is typically called with moduleSourcePath=null and falls back to the external-module cache (likely returning null).
That means custom modules may always show version: null in manifests even when the cached repo has a .claude-plugin/marketplace.json.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| version: customVersion, | ||
| source: 'custom', | ||
| npmPackage: null, | ||
| repoUrl: null, |
There was a problem hiding this comment.
tools/installer/core/manifest.js:845 — For custom modules, repoUrl is always recorded as null, which makes it hard to trace provenance and can be ambiguous if multiple cached custom repos expose the same plugin name.
Consider capturing the source URL from the custom cache/marketplace metadata for the manifest.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
📝 WalkthroughWalkthroughThis pull request extends the installer system with support for community and custom modules. New module managers are introduced to fetch and manage community modules from a GitHub registry and custom modules from user-provided GitHub URLs. The manifest resolution is updated to discover and resolve these sources. The module selection UI is reworked into a three-phase pipeline allowing users to select from official, community, and custom modules. HTTP fetching is centralized via a new RegistryClient, which ExternalModuleManager now delegates to. Config is updated to track custom modules metadata. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant UI
participant OfficialModules
participant CommunityModuleManager
participant CustomModuleManager
participant RegistryClient
participant GitHub
User->>UI: Start module selection
rect rgba(100, 150, 200, 0.5)
Note over UI,OfficialModules: Phase 1: Select Official Modules
UI->>OfficialModules: _selectOfficialModules()
OfficialModules-->>UI: [official module selections]
end
rect rgba(150, 100, 200, 0.5)
Note over UI,CommunityModuleManager: Phase 2: Browse Community Modules
UI->>CommunityModuleManager: loadCategories()
CommunityModuleManager->>RegistryClient: fetchYaml(community-index)
RegistryClient->>GitHub: GET community registry
GitHub-->>RegistryClient: YAML content
RegistryClient-->>CommunityModuleManager: parsed index
CommunityModuleManager-->>UI: categories list
loop Category browsing
User->>UI: Select category
UI->>CommunityModuleManager: listByCategory(slug)
CommunityModuleManager-->>UI: [modules in category]
User->>UI: Select modules
end
UI-->>UI: [community module selections]
end
rect rgba(200, 150, 100, 0.5)
Note over UI,CustomModuleManager: Phase 3: Add Custom GitHub URLs
loop URL entry loop
User->>UI: Enter GitHub URL
UI->>CustomModuleManager: Fetch repo plugins
CustomModuleManager->>RegistryClient: fetchJson(.claude-plugin/marketplace.json)
RegistryClient->>GitHub: GET marketplace.json
GitHub-->>RegistryClient: JSON content
RegistryClient-->>CustomModuleManager: plugin definitions
CustomModuleManager->>GitHub: Clone/update repo
GitHub-->>CustomModuleManager: local cache
CustomModuleManager-->>UI: [discovered plugins]
User->>UI: Confirm plugin selection
UI-->>UI: [custom module selections]
end
end
UI-->>User: [merged all selections]
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
tools/installer/ui.js (1)
618-625:⚠️ Potential issue | 🟠 MajorLet users skip official modules in phase 1.
required: trueforces at least one official selection before the community/custom phases even start. That makes community-only and custom-only installs impossible on a fresh project. If you still want a guard, enforce it after all three phases are complete.💡 Proposed fix
- required: true, + required: false,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tools/installer/ui.js` around lines 618 - 625, The prompts.autocompleteMultiselect call (assigned to selected) currently sets required: true which forces at least one official module selection and prevents community-only or custom-only installs; change this by removing or setting required to false on the prompts.autocompleteMultiselect options and, if you need a guard, perform validation after all three phases (official, community, custom) are completed rather than blocking at this step so a user can skip official modules during phase 1.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tools/installer/core/manifest.js`:
- Around line 835-846: The custom-module branch currently finds customSource via
CustomModuleManager but still calls _readMarketplaceVersion(moduleName,
moduleSourcePath), which can be null and causes version lookup to use the wrong
cache; change the call to pass the discovered customSource (or its path/URL
value) into _readMarketplaceVersion so the helper reads version metadata from
the custom module location. Update the block around
CustomModuleManager/customMgr/customSource and the return that sets version to
use the value returned by _readMarketplaceVersion when invoked with customSource
instead of moduleSourcePath.
In `@tools/installer/modules/community-manager.js`:
- Around line 181-183: Validate and sanitize registry-derived fields before
using them: ensure moduleCode contains only allowed characters (e.g.,
alphanumerics, hyphen, underscore) and does not contain path separators or
traversal sequences, then build moduleCacheDir by resolving
path.join(this.getCacheDir(), moduleCode) and verifying the resolved path starts
with the cacheDir prefix; validate moduleInfo.url is a well-formed git URL (or
matches an allowed host pattern) and approvedSha matches a safe hex commit-ish
pattern before using them; replace execSync shell invocations that interpolate
url/sha with execFileSync or spawnSync using argument arrays (e.g., ['git',
'clone', moduleInfo.url, moduleCacheDir'] and ['git', 'checkout', approvedSha'])
to avoid shell injection, and apply the same validation and execFileSync
refactor to the other occurrences around lines 229-255 (the git clone/checkout
flows).
- Around line 303-307: The code currently uses path.join(cloneDir,
moduleInfo.moduleDefinition) and returns path.dirname(configuredPath) without
ensuring the joined path is inside the cloned repo; resolve the joined path
(e.g., with path.resolve) and verify the resolved configuredPath is still under
cloneDir (e.g., compare path.relative(cloneDir, resolvedPath) does not start
with '..' or use startsWith after normalizing) before using or returning it; if
the check fails, treat it as not found and continue. Apply the same fix for the
other occurrence referenced (around line 336) that uses
moduleInfo.moduleDefinition and returning path.dirname.
In `@tools/installer/modules/custom-module-manager.js`:
- Around line 25-45: The validateGitHubUrl function is too permissive and allows
traversal/injection vectors; update validateGitHubUrl to parse the URL
structurally (use URL parsing for https and explicit parsing for git@ SSH) and
strictly validate that owner and repo match a safe whitelist regex (e.g. only
alphanumerics, hyphen, underscore and dot: /^[A-Za-z0-9._-]+$/), explicitly
reject '.' and '..', trim any .git suffix from repo, and return isValid=false
with an error for invalid values; then update call sites that use
path.join(cacheDir, owner, repo) (references around the code using path.join at
lines ~110-112) to assume owner/repo are sanitized, and replace execSync("git
clone ...") uses (references around ~148-155) with a non-shell invocation (e.g.
child_process.execFile or spawn with args) to pass "git" and ["clone", repoUrl,
targetPath"] as separate arguments to avoid shell injection.
In `@tools/installer/ui.js`:
- Around line 833-865: The code currently pushes only plugin.code into
selectedModules, losing source metadata and risking collisions; change the
selection step (inside the loop that iterates plugins after cloning) to push an
object that includes code, repoUrl (use the url.trim() used earlier), and source
(e.g., 'custom' or a value consistent with customModulesMeta) instead of a
string; ensure the rest of the flow that consumes selectedModules is updated to
expect objects (or populate the new customModulesMeta field accordingly) so
downstream resolution uses the stored repoUrl/source rather than scanning the
cache by code.
- Around line 647-651: The current prompt handler returns [] when the user
declines "browseCommunity", which drops installed community/custom modules;
update the flow to return the existing installedModuleIds if present instead of
an empty array—i.e., where the code sets const browseCommunity and then does if
(!browseCommunity) return [], change that branch to return installedModuleIds ||
[] (or otherwise merge/retain installedModuleIds) so installed community/custom
modules are preserved in modify/update mode; apply the same change to the
duplicate block referencing browseCommunity around the other occurrence (lines
~807-811).
---
Outside diff comments:
In `@tools/installer/ui.js`:
- Around line 618-625: The prompts.autocompleteMultiselect call (assigned to
selected) currently sets required: true which forces at least one official
module selection and prevents community-only or custom-only installs; change
this by removing or setting required to false on the
prompts.autocompleteMultiselect options and, if you need a guard, perform
validation after all three phases (official, community, custom) are completed
rather than blocking at this step so a user can skip official modules during
phase 1.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 5bb4b141-8316-4876-b6cf-caadddfa84d7
📒 Files selected for processing (8)
tools/installer/core/config.jstools/installer/core/manifest.jstools/installer/modules/community-manager.jstools/installer/modules/custom-module-manager.jstools/installer/modules/external-manager.jstools/installer/modules/official-modules.jstools/installer/modules/registry-client.jstools/installer/ui.js
| // Check if this is a custom module (from user-provided URL) | ||
| const { CustomModuleManager } = require('../modules/custom-module-manager'); | ||
| const customMgr = new CustomModuleManager(); | ||
| const customSource = await customMgr.findModuleSourceByCode(moduleName); | ||
| if (customSource) { | ||
| const customVersion = await this._readMarketplaceVersion(moduleName, moduleSourcePath); | ||
| return { | ||
| version: customVersion, | ||
| source: 'custom', | ||
| npmPackage: null, | ||
| repoUrl: null, | ||
| }; |
There was a problem hiding this comment.
Use customSource when loading the custom module version.
This branch already found the module in the custom cache, but _readMarketplaceVersion() still receives moduleSourcePath, which is usually null in status/update flows. That makes the helper fall back to the external-module cache and custom modules lose their version metadata.
💡 Proposed fix
- const customVersion = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
+ const customVersion = await this._readMarketplaceVersion(moduleName, moduleSourcePath || customSource);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Check if this is a custom module (from user-provided URL) | |
| const { CustomModuleManager } = require('../modules/custom-module-manager'); | |
| const customMgr = new CustomModuleManager(); | |
| const customSource = await customMgr.findModuleSourceByCode(moduleName); | |
| if (customSource) { | |
| const customVersion = await this._readMarketplaceVersion(moduleName, moduleSourcePath); | |
| return { | |
| version: customVersion, | |
| source: 'custom', | |
| npmPackage: null, | |
| repoUrl: null, | |
| }; | |
| // Check if this is a custom module (from user-provided URL) | |
| const { CustomModuleManager } = require('../modules/custom-module-manager'); | |
| const customMgr = new CustomModuleManager(); | |
| const customSource = await customMgr.findModuleSourceByCode(moduleName); | |
| if (customSource) { | |
| const customVersion = await this._readMarketplaceVersion(moduleName, moduleSourcePath || customSource); | |
| return { | |
| version: customVersion, | |
| source: 'custom', | |
| npmPackage: null, | |
| repoUrl: null, | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tools/installer/core/manifest.js` around lines 835 - 846, The custom-module
branch currently finds customSource via CustomModuleManager but still calls
_readMarketplaceVersion(moduleName, moduleSourcePath), which can be null and
causes version lookup to use the wrong cache; change the call to pass the
discovered customSource (or its path/URL value) into _readMarketplaceVersion so
the helper reads version metadata from the custom module location. Update the
block around CustomModuleManager/customMgr/customSource and the return that sets
version to use the value returned by _readMarketplaceVersion when invoked with
customSource instead of moduleSourcePath.
| const cacheDir = this.getCacheDir(); | ||
| const moduleCacheDir = path.join(cacheDir, moduleCode); | ||
| const silent = options.silent || false; |
There was a problem hiding this comment.
Validate registry fields before using them in paths and shell commands.
moduleCode, moduleInfo.url, and approvedSha all come from remote marketplace data here. moduleCode is used to build a local path, and url/sha are interpolated into execSync(...); a malformed registry entry can escape the cache directory or break out of the git command line locally. Validate these fields first, and call git with argv arrays (execFileSync/spawnSync) instead of shell strings.
Also applies to: 229-255
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tools/installer/modules/community-manager.js` around lines 181 - 183,
Validate and sanitize registry-derived fields before using them: ensure
moduleCode contains only allowed characters (e.g., alphanumerics, hyphen,
underscore) and does not contain path separators or traversal sequences, then
build moduleCacheDir by resolving path.join(this.getCacheDir(), moduleCode) and
verifying the resolved path starts with the cacheDir prefix; validate
moduleInfo.url is a well-formed git URL (or matches an allowed host pattern) and
approvedSha matches a safe hex commit-ish pattern before using them; replace
execSync shell invocations that interpolate url/sha with execFileSync or
spawnSync using argument arrays (e.g., ['git', 'clone', moduleInfo.url,
moduleCacheDir'] and ['git', 'checkout', approvedSha']) to avoid shell
injection, and apply the same validation and execFileSync refactor to the other
occurrences around lines 229-255 (the git clone/checkout flows).
| if (moduleInfo.moduleDefinition) { | ||
| const configuredPath = path.join(cloneDir, moduleInfo.moduleDefinition); | ||
| if (await fs.pathExists(configuredPath)) { | ||
| return path.dirname(configuredPath); | ||
| } |
There was a problem hiding this comment.
Keep moduleDefinition inside the cloned repo.
moduleInfo.moduleDefinition is registry-controlled, and path.join(cloneDir, moduleInfo.moduleDefinition) will walk out of cloneDir when it contains ... That lets a marketplace entry redirect installation to some other local directory that happens to contain a module.yaml. Resolve the path and verify it still sits under cloneDir before using or returning it.
Also applies to: 336-336
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tools/installer/modules/community-manager.js` around lines 303 - 307, The
code currently uses path.join(cloneDir, moduleInfo.moduleDefinition) and returns
path.dirname(configuredPath) without ensuring the joined path is inside the
cloned repo; resolve the joined path (e.g., with path.resolve) and verify the
resolved configuredPath is still under cloneDir (e.g., compare
path.relative(cloneDir, resolvedPath) does not start with '..' or use startsWith
after normalizing) before using or returning it; if the check fails, treat it as
not found and continue. Apply the same fix for the other occurrence referenced
(around line 336) that uses moduleInfo.moduleDefinition and returning
path.dirname.
| validateGitHubUrl(url) { | ||
| if (!url || typeof url !== 'string') { | ||
| return { owner: null, repo: null, isValid: false, error: 'URL is required' }; | ||
| } | ||
|
|
||
| const trimmed = url.trim(); | ||
|
|
||
| // HTTPS format: https://github.com/owner/repo[.git] | ||
| const httpsMatch = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/); | ||
| if (httpsMatch) { | ||
| return { owner: httpsMatch[1], repo: httpsMatch[2], isValid: true, error: null }; | ||
| } | ||
|
|
||
| // SSH format: git@github.com:owner/repo.git | ||
| const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/); | ||
| if (sshMatch) { | ||
| return { owner: sshMatch[1], repo: sshMatch[2], isValid: true, error: null }; | ||
| } | ||
|
|
||
| return { owner: null, repo: null, isValid: false, error: 'Not a valid GitHub URL (expected https://github.com/owner/repo)' }; | ||
| } |
There was a problem hiding this comment.
validateGitHubUrl() is too permissive for later path/shell use.
These regexes currently accept values like .., quotes, spaces, and query fragments in the owner/repo segments, and those values are later reused in path.join(cacheDir, owner, repo) and execSync("git clone ..."). That is a traversal/injection hazard, and it also rejects valid dotted repo names. Parse the URL structurally, reject ./.. and non-GitHub-safe characters, and stop invoking git through a shell.
Also applies to: 110-112, 148-155
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tools/installer/modules/custom-module-manager.js` around lines 25 - 45, The
validateGitHubUrl function is too permissive and allows traversal/injection
vectors; update validateGitHubUrl to parse the URL structurally (use URL parsing
for https and explicit parsing for git@ SSH) and strictly validate that owner
and repo match a safe whitelist regex (e.g. only alphanumerics, hyphen,
underscore and dot: /^[A-Za-z0-9._-]+$/), explicitly reject '.' and '..', trim
any .git suffix from repo, and return isValid=false with an error for invalid
values; then update call sites that use path.join(cacheDir, owner, repo)
(references around the code using path.join at lines ~110-112) to assume
owner/repo are sanitized, and replace execSync("git clone ...") uses (references
around ~148-155) with a non-shell invocation (e.g. child_process.execFile or
spawn with args) to pass "git" and ["clone", repoUrl, targetPath"] as separate
arguments to avoid shell injection.
tools/installer/ui.js
Outdated
| const browseCommunity = await prompts.confirm({ | ||
| message: 'Would you like to browse community modules?', | ||
| default: false, | ||
| }); | ||
| if (!browseCommunity) return []; |
There was a problem hiding this comment.
Skipping these prompts drops installed community/custom modules from modify flows.
In update mode installedModuleIds is already available, but answering “no” here returns []. Official modules keep their installed state through phase 1 initialValues; community/custom modules do not, so they fall out of the desired module list unless the user re-enters these flows.
Also applies to: 807-811
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tools/installer/ui.js` around lines 647 - 651, The current prompt handler
returns [] when the user declines "browseCommunity", which drops installed
community/custom modules; update the flow to return the existing
installedModuleIds if present instead of an empty array—i.e., where the code
sets const browseCommunity and then does if (!browseCommunity) return [], change
that branch to return installedModuleIds || [] (or otherwise merge/retain
installedModuleIds) so installed community/custom modules are preserved in
modify/update mode; apply the same change to the duplicate block referencing
browseCommunity around the other occurrence (lines ~807-811).
| const plugins = await customMgr.discoverModules(url.trim()); | ||
| s.stop('Module info loaded'); | ||
|
|
||
| await prompts.log.warn( | ||
| 'UNVERIFIED MODULE: This module has not been reviewed by the BMad team.\n' + ' Only install modules from sources you trust.', | ||
| ); | ||
|
|
||
| for (const plugin of plugins) { | ||
| const versionStr = plugin.version ? ` v${plugin.version}` : ''; | ||
| await prompts.log.info(` ${plugin.name}${versionStr}\n ${plugin.description}\n Author: ${plugin.author}`); | ||
| } | ||
|
|
||
| const confirmInstall = await prompts.confirm({ | ||
| message: `Install ${plugins.length} plugin${plugins.length === 1 ? '' : 's'} from ${url.trim()}?`, | ||
| default: false, | ||
| }); | ||
|
|
||
| if (confirmInstall) { | ||
| // Pre-clone the repo so it's cached for the install pipeline | ||
| s.start('Cloning repository...'); | ||
| try { | ||
| await customMgr.cloneRepo(url.trim()); | ||
| s.stop('Repository cloned'); | ||
| } catch (cloneError) { | ||
| s.error('Failed to clone repository'); | ||
| await prompts.log.error(` ${cloneError.message}`); | ||
| addMore = await prompts.confirm({ message: 'Try another URL?', default: false }); | ||
| continue; | ||
| } | ||
|
|
||
| for (const plugin of plugins) { | ||
| selectedModules.push(plugin.code); | ||
| } |
There was a problem hiding this comment.
Keep custom selection metadata, not just plugin.code.
This flow flattens each discovered plugin down to its code. Downstream resolution then has to scan the entire custom cache by code and take the first match, so two repos with the same plugin name — or a collision with an official/community code — can resolve to the wrong source. Persist { code, repoUrl, source } here; the new customModulesMeta field looks intended for that.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tools/installer/ui.js` around lines 833 - 865, The code currently pushes only
plugin.code into selectedModules, losing source metadata and risking collisions;
change the selection step (inside the loop that iterates plugins after cloning)
to push an object that includes code, repoUrl (use the url.trim() used earlier),
and source (e.g., 'custom' or a value consistent with customModulesMeta) instead
of a string; ensure the rest of the flow that consumes selectedModules is
updated to expect objects (or populate the new customModulesMeta field
accordingly) so downstream resolution uses the stored repoUrl/source rather than
scanning the cache by code.
- Remove redundant CommunityModuleManager instantiation in UI display - Remove dead customModulesMeta field from Config (never populated) - Add 35 unit tests for CustomModuleManager and CommunityModuleManager pure functions: URL validation, normalization, search, featured, categories
When a user does "Modify Installation" and declines to browse community modules, previously installed community/custom modules are now auto-kept. If the user does browse, their selections are trusted (they can deselect). Also fix stale docs: class doc for SHA pinning, JSDoc return type.
Quick update now checks community registry and custom cache so installed community/custom modules are updated instead of skipped.
When quick update encounters new config fields (e.g., from a newly supported community module), use schema defaults silently instead of prompting the user. Quick update should be non-interactive.
…e cases Cover SHA normalization (set vs null/trusted), listByCategory, getModuleByCode, and URL validation edge cases (HTTP, trailing slash, SSH without .git). Total: 243 tests.
Summary
community-index.yaml, pinned toapproved_shawhen set (refuses install if SHA unreachable), uses HEAD when no SHA (trusted contributors).claude-plugin/marketplace.jsonwith unverified warningRegistryClientutility for HTTP fetch/YAML/JSON parsingfindModuleSourcechain extended with community and custom fallthroughcommunityandcustomsource typesNew files
tools/installer/modules/registry-client.js- Shared HTTPS fetchtools/installer/modules/community-manager.js- Community catalog, categories, SHA-pinned cloningtools/installer/modules/custom-module-manager.js- GitHub URL validation, marketplace.json discoveryTest plan