MusePi

Plugin manager and installer plumbing

This document describes how musepi plugin npm/git/link and marketplace operations mutate plugin state on disk and become runtime capabilities. Marketplace installs keep their own registries and cache, then register the cached plugin through the same node_modules and musepi-plugins.lock.json runtime surfaces used by npm/git/link installs; see docs/marketplace.md.

Scope and architecture

There are two plugin-management implementations in the codebase:

  1. Active path used by CLI commands: PluginManager (src/extensibility/plugins/manager.ts)
  2. Legacy helper module: installer functions (src/extensibility/plugins/installer.ts)

musepi plugin npm/git/link actions go through PluginManager; marketplace actions go through MarketplaceManager. install classifies each target (classifyInstallTarget in cli/classify-install-target.ts): name@marketplace routes to the marketplace manager, local paths route to PluginManager.link(), git and npm specs to PluginManager.install().

installer.ts still documents important safety checks and filesystem behavior, but it is not the path used by src/commands/plugin.ts + src/cli/plugin-cli.ts.

Lifecycle: from CLI invocation to runtime availability

musepi plugin <npm/link action> ...
  -> src/commands/plugin.ts
  -> runPluginCommand(...) in src/cli/plugin-cli.ts
  -> PluginManager method (install/list/uninstall/link/...)
  -> mutate ~/.musepi/plugins/{package.json,node_modules,musepi-plugins.lock.json}
  -> runtime discovery: discoverAndLoadCustomTools(...) and discoverAndLoadExtensions(...)
  -> getAllPluginToolPaths(cwd) / getAllPluginExtensionPaths(cwd)
  -> custom tool loader imports tool modules; extension loader imports extension modules

musepi plugin install name@marketplace / musepi install name@marketplace
  -> MarketplaceManager
  -> mutate marketplace registries and cache
  -> symlink the cached package into the scope's node_modules and update musepi-plugins.lock.json
  -> plugin-root discovery loads skills/commands/etc.; runtime loaders import tools and extensions

Command entrypoints

On-disk model

Global plugin state lives under ~/.musepi/plugins:

Project-local overrides live at:

Overrides are read-only from manager/loader perspective (no write path here) and can disable plugins or override features/settings for this project.

Marketplace installs add registry and cache state alongside those runtime entries:

Plugin spec parsing and metadata interpretation

Install spec grammar

parsePluginSpec (parser.ts) supports:

PluginManager.install also accepts git sources (validated by validateGitSpec instead of the npm regex): namespaced shorthands github:user/repo[#ref], gitlab:, bitbucket:, codeberg:, sourcehut:/srht:, and full git URLs (https://github.com/user/repo, git@github.com:user/repo, ssh://…, git+https://…). Git specs do not encode the package name, so install diffs plugins/package.json#dependencies before/after bun install to resolve it.

extractPackageName strips version suffix for on-disk path lookup after install.

Manifest source and required fields

Manifest is resolved as:

  1. package.json.musepi
  2. fallback package.json.pi
  3. fallback { version: package.version }

Implications:

Malformed package.json JSON is a hard failure at read time; malformed manifest shape may fail later only when specific fields are consumed.

Install/update flow (PluginManager.install)

  1. Parse feature bracket syntax from install spec.
  2. Validate the spec: git specs via validateGitSpec; npm specs against the package-name regex + shell-metacharacter denylist.
  3. Ensure plugin package.json exists (omp-plugins, private dependencies map).
  4. Run bun install <packageSpec> in ~/.musepi/plugins.
  5. Resolve the installed package name (npm: strip version via extractPackageName; git: diff dependencies before/after) and read node_modules/<name>/package.json.
  6. Resolve manifest and compute enabledFeatures:
    • [*]: all declared features (or null if no feature map)
    • [a,b]: validates each feature exists in manifest features map
    • []: empty feature list
    • bare spec: null (use defaults policy later in loader)
  7. Validate declared extension entries (#validateInstalledExtensions): each manifest extensions entry must resolve on disk, import to a factory function, and initialize successfully against a throwaway registration surface. On failure, roll back the install — restore the previous plugins/package.json, remove the freshly installed package, and restore any prior version from a backup taken before bun install — then abort.
  8. Upsert lockfile runtime state: { version, enabledFeatures, enabled: true }.

Update semantics

Because update is install-driven:

Remove flow (PluginManager.uninstall)

  1. Validate package name.
  2. Run bun uninstall <name> in plugin dir.
  3. Remove plugin runtime state from lockfile:
    • config.plugins[name]
    • config.settings[name]

If uninstall command fails, runtime state is not changed.

List flow (PluginManager.list)

  1. Read plugin dependency map from ~/.musepi/plugins/package.json.
  2. Load lockfile runtime config (missing file -> empty defaults).
  3. Load project overrides (<cwd>/.musepi/plugin-overrides.json, parse/read errors -> empty object with warning).
  4. For each dependency with a resolvable package.json:
    • build InstalledPlugin record
    • merge feature/enable state:
      • base from lockfile (or defaults)
      • project overrides can replace feature selection
      • project disabled list masks plugin as disabled

This is the effective state used by CLI status output and settings/features operations.

link supports local plugin development by symlinking a local package into ~/.musepi/plugins/node_modules/<pkg.name>.

Behavior:

  1. Resolve localPath against manager cwd.
  2. Require local package.json and name field.
  3. Ensure plugin dirs exist.
  4. For scoped names, create scope directory.
  5. Remove existing path at target link location.
  6. Create symlink.
  7. Add runtime lockfile entry enabled with default features (null).

Caveat: current PluginManager.link does not enforce the cwd path-boundary check present in legacy installer.ts (normalizedPath.startsWith(normalizedCwd)), so trust is the caller’s responsibility.

Runtime loading: from installed plugin to callable capabilities

Discovery gate

getEnabledPlugins(cwd) (plugins/loader.ts) reads:

Filtering:

Capability path resolution

For each enabled plugin:

Each resolver includes base entries plus feature entries:

Manifest entries may point to a file or to a directory containing index.ts, index.js, index.mjs, or index.cjs. Missing files are silently skipped (statSync/existsSync guard).

Current runtime wiring differences

Lock/state management details

PluginManager caches runtime config in memory per instance (#runtimeConfig) and lazily loads once.

Load behavior:

Save behavior:

No cross-process locking or merge strategy exists; concurrent writers can overwrite each other.

Safety checks and trust boundaries

Input/package validation

Active manager path enforces package-name validation:

This limits command-injection risk when invoking bun install/uninstall.

Filesystem trust boundary

Legacy installer-only checks

installer.ts includes additional link-time checks not mirrored in PluginManager.link:

Because CLI uses PluginManager, these stricter link guards are not currently on the main path.

Failure, partial success, and rollback behavior

The plugin manager is not transactional.

Operation stage Failure behavior Rollback
bun install fails install aborts with stderr N/A (no state writes yet)
Install succeeds, then feature validation fails command fails No uninstall rollback; dependency may remain in node_modules/package.json
Install succeeds, then extension validation fails command fails Rolls back: restores package.json, removes installed package, restores prior version from backup
Install succeeds, then lockfile write fails command fails No rollback of installed package
bun uninstall succeeds, lockfile write fails command fails Package removed, stale runtime state may remain
link removes old target then symlink creation fails command fails No restoration of previous link/dir

Operationally, doctor --fix can repair some drift (bun install, orphaned config cleanup, invalid-feature cleanup), but it is best-effort.

Malformed/missing manifest behavior summary

Mode differences and precedence

Implementation files