This document describes the implementation of plugin and marketplace management commands for Construct.
| Path | Description |
|---|---|
~/.claude/plugins/marketplaces/ |
Default clone location for git-based marketplaces |
~/.claude/plugins/known_marketplaces.json |
Registry of all known marketplaces |
$PWD/.construct.json |
Project-local enabled plugins config |
interface KnownMarketplacesFile {
[marketplaceName: string]: {
source: {
source: "github" | "directory";
repo?: string; // For github: "owner/repo"
path?: string; // For directory: absolute path
};
installLocation: string; // Absolute path to marketplace root
lastUpdated: string; // ISO 8601 timestamp
};
}Example:
{
"claude-plugins-official": {
"source": { "source": "github", "repo": "anthropics/claude-plugins-official" },
"installLocation": "/Users/mike/.claude/plugins/marketplaces/claude-plugins-official",
"lastUpdated": "2026-01-15T01:38:49.056Z"
},
"scaryrawr-plugins": {
"source": { "source": "directory", "path": "/Users/mike/GitHub/claude-plugins" },
"installLocation": "/Users/mike/GitHub/claude-plugins",
"lastUpdated": "2026-01-14T16:02:05.636Z"
}
}| File | Purpose |
|---|---|
src/plugin.ts |
Business logic for plugin enable/disable |
src/marketplace.ts |
Business logic for marketplace add/remove/update/list |
src/plugin.test.ts |
Unit tests for plugin operations |
src/marketplace.test.ts |
Unit tests for marketplace operations |
Extend parseCliArgs() to support yargs subcommands:
export interface CliArgs {
command: "run" | "operator" | "plugin";
// ... existing fields ...
pluginSubcommand?: "enable" | "disable";
listEnabled?: boolean;
marketplaceSubcommand?: "list" | "add" | "remove" | "update";
pluginName?: string;
marketplaceTarget?: string; // marketplace name or URL
updateAll?: boolean;
}Use yargs .command() for nested subcommands.
| Scenario | Behavior |
|---|---|
| Plugin not found in any marketplace | console.error("Error: Plugin \"<name>\" not found in any known marketplace") + process.exit(1) |
| Marketplace already exists | Update the existing marketplace (git pull) and continue successfully |
| Network failure (clone/update) | console.error("Error: Failed to clone/update marketplace: <details>") + process.exit(1) |
| Invalid marketplace URL/name | console.error("Error: Invalid marketplace: <input>") + process.exit(1) |
Usage:
construct plugin enable <plugin-name>@<marketplace-name>Example:
construct plugin enable tmux@scaryrawr-pluginsImplementation (src/plugin.ts):
export async function enablePlugin(pluginName: string): Promise<void>- Call
scanAllPlugins()fromscanner.tsto get the plugin registry - Check if
pluginNameexists inregistry.plugins- If not found:
console.error()andprocess.exit(1)
- If not found:
- Call
loadConfig()fromconfig.ts - If plugin already in
enabledPlugins, print info message and return - Add plugin to
enabledPluginsarray - Call
saveConfig()with updated config - Print success:
console.log("Enabled plugin: <name>")
Unit Tests (src/plugin.test.ts):
enablePlugin() adds plugin to .construct.json when plugin existsenablePlugin() exits with error when plugin not foundenablePlugin() is idempotent (no duplicate entries)
Usage:
construct plugin disable <plugin-name>@<marketplace-name>Example:
construct plugin disable tmux@scaryrawr-pluginsImplementation (src/plugin.ts):
export async function disablePlugin(pluginName: string): Promise<void>- Call
loadConfig()fromconfig.ts - If config is null or plugin not in
enabledPlugins, print info and return - Filter out the plugin from
enabledPlugins - Call
saveConfig()with updated config - Print success:
console.log("Disabled plugin: <name>")
Unit Tests (src/plugin.test.ts):
disablePlugin() removes plugin from .construct.jsondisablePlugin() handles missing .construct.json gracefullydisablePlugin() handles plugin not in config gracefully
Usage:
construct plugin --list-enabled
construct plugin -eImplementation (src/plugin.ts):
export async function listEnabledPlugins(): Promise<void>- Call
loadConfig()fromconfig.ts - If config is null or
enabledPluginsis empty, print "No plugins enabled." and return - Print header:
console.log("Enabled plugins:") - For each plugin in
enabledPlugins, print:<plugin-name>@<marketplace-name>
Output format:
Enabled plugins:
tmux@scaryrawr-plugins
playwright@claude-plugins-official
Unit Tests (src/plugin.test.ts):
listEnabledPlugins() prints all enabled plugins from .construct.jsonlistEnabledPlugins() handles missing .construct.json gracefullylistEnabledPlugins() handles empty enabledPlugins array
Usage:
construct plugin marketplace --list
construct plugin marketplace -lImplementation (src/marketplace.ts):
export async function listMarketplaces(): Promise<void>- Read
~/.claude/plugins/known_marketplaces.json- If file doesn't exist, print "No marketplaces configured." and return
- For each marketplace, print:
<name> (<source.source>)
Output format:
Known marketplaces:
claude-plugins-official (github)
scaryrawr-plugins (directory)
Unit Tests (src/marketplace.test.ts):
listMarketplaces() prints all known marketplaceslistMarketplaces() handles missing known_marketplaces.json
Usage:
# Full GitHub URL
construct plugin marketplace add https://github.com/owner/repo-name
# GitHub shorthand (owner/repo)
construct plugin marketplace add scaryrawr/scaryrawr-pluginsImplementation (src/marketplace.ts):
export async function addMarketplace(target: string): Promise<void>- Parse
targetto determine source type:- If starts with
https://github.com/: extractowner/repo, type = "github" - If matches
owner/repopattern: type = "github" - Otherwise:
console.error("Invalid marketplace: ...")+process.exit(1)
- If starts with
- Derive marketplace name from repo (last segment, e.g., "scaryrawr-plugins")
- Check if marketplace already exists in known_marketplaces.json:
- If exists with same source: run
git pullto update, print success, return - If exists with different source:
console.error()+process.exit(1)
- If exists with same source: run
- Clone repo to
~/.claude/plugins/marketplaces/<name>/- Use:
git clone https://github.com/<owner>/<repo>.git <path> - On failure:
console.error("Failed to clone: <error>")+process.exit(1)
- Use:
- Validate marketplace has
.claude-plugin/marketplace.json- If missing: remove cloned dir,
console.error()+process.exit(1)
- If missing: remove cloned dir,
- Add entry to known_marketplaces.json
- Print success:
console.log("Added marketplace: <name>")
Unit Tests (src/marketplace.test.ts):
addMarketplace() parses full GitHub URL correctlyaddMarketplace() parses owner/repo shorthand correctlyaddMarketplace() rejects invalid inputaddMarketplace() updates existing marketplace instead of erroring
Usage:
construct plugin marketplace remove <marketplace-name>Example:
construct plugin marketplace remove scaryrawr-pluginsImplementation (src/marketplace.ts):
export async function removeMarketplace(name: string): Promise<void>- Read known_marketplaces.json
- If marketplace not found:
console.error()+process.exit(1) - Get marketplace info
- If
source.source === "github":- Delete the
installLocationdirectory recursively
- Delete the
- If
source.source === "directory":- Do NOT delete the directory (it's a user path)
- Just remove the registry entry
- Remove entry from known_marketplaces.json and save
- Print success:
console.log("Removed marketplace: <name>")
Unit Tests (src/marketplace.test.ts):
removeMarketplace() deletes git-cloned marketplace from diskremoveMarketplace() preserves directory-based marketplace on diskremoveMarketplace() removes entry from known_marketplaces.jsonremoveMarketplace() errors when marketplace not found
Usage:
# Update all git-based marketplaces
construct plugin marketplace update --all
construct plugin marketplace update -a
# Update specific marketplace
construct plugin marketplace update <marketplace-name>Examples:
construct plugin marketplace update --all
construct plugin marketplace update scaryrawr-pluginsImplementation (src/marketplace.ts):
export async function updateMarketplace(name: string): Promise<void>
export async function updateAllMarketplaces(): Promise<void>updateMarketplace(name):
- Read known_marketplaces.json
- If marketplace not found:
console.error()+process.exit(1) - If
source.source !== "github": print info "Skipping directory-based marketplace" and return - Run
git -C <installLocation> pull- On failure:
console.error()+process.exit(1)
- On failure:
- Update
lastUpdatedtimestamp in known_marketplaces.json - Print success:
console.log("Updated marketplace: <name>")
updateAllMarketplaces():
- Read known_marketplaces.json
- For each marketplace where
source.source === "github":- Call
updateMarketplace(name)
- Call
- Print summary:
console.log("Updated <n> marketplace(s)")
Unit Tests (src/marketplace.test.ts):
updateMarketplace() runs git pull on github marketplaceupdateMarketplace() skips directory-based marketplaceupdateMarketplace() updates lastUpdated timestampupdateAllMarketplaces() updates all git-based marketplaces
Run tests with:
bun test src/plugin.test.ts
bun test src/marketplace.test.tsUse temp directories and mock files (see src/cache.test.ts for patterns):
- Create temp known_marketplaces.json files
- Create temp .construct.json files
- Use
beforeEach/afterEachfor setup/cleanup
For git operations in tests, consider:
- Mocking
Bun.spawn()or using a test git repo - Using
--dry-runflags where available