MMGIS uses a plugin-based architecture for tools, backend modules, and components. Plugins are organized under /plugins/ in a three-level hierarchy: <container>/<type>/<PluginName>/.
- Directory Structure
- Key Files
- Quick Start
- CLI Commands
- Plugin Types
- Installing Plugins
- Creating Plugins
plugin.jsonReference- Discovery & State
- Webpack Aliases
- Validation
- Registries
- Testing Plugins
- Migrating from Legacy Formats
- AI Agent Notes
plugins/
├── plugin-cli.js # CLI tool for plugin management
├── plugin-registries.json # Registered plugin sources (git URLs, local paths)
├── plugin-state.json # Enable/disable state per plugin (gitignored)
├── README.md # This file
├── core/ # Core plugins (committed, version-controlled)
│ ├── tools/ # Frontend tools (Draw, Measure, Legend, etc.)
│ ├── backend/ # Server modules (Accounts, Config, Users, etc.)
│ └── components/ # UI components (OperationsClock, etc.)
└── <org--repo>/ # Installed from git (org--repo naming, gitignored)
├── tools/
├── backend/
└── components/
| File | Purpose |
|---|---|
plugins/plugin-cli.js |
CLI for plugin management (npm run plugins -- <cmd>) |
plugins/plugin-registries.json |
Git URLs / local paths of known plugin sources |
plugins/plugin-state.json |
Enable/disable state (gitignored, instance-specific) |
API/pluginDiscovery.js |
Discovery logic — discoverPlugins() scans all containers |
API/pluginValidation.js |
Manifest validation — validatePluginConfig() |
API/updateTools.js |
Build-time tool/component discovery → generates src/pre/tools.js |
API/setups.js |
Runtime backend discovery → loads plugin.js lifecycle hooks |
scripts/resolve-plugin-deps.js |
Aggregates plugin dependencies for build |
# List all plugins
npm run plugins -- list
# Create a new plugin
npm run plugins -- create tool MyTool --container my-plugins
# Install from a git repo (clones to plugins/org--mmgis-geo-plugins/)
npm run plugins -- install https://github.com/org/mmgis-geo-plugins.git
# Enable/disable a specific plugin
npm run plugins -- enable my-plugins/tools/SpectralTool
npm run plugins -- disable SpectralTool
# Validate all plugin manifests
npm run plugins -- validate
# Show dependency graph
npm run plugins -- deps
# Plugin details
npm run plugins -- info Draw
# Machine-readable output (works with all commands)
npm run plugins -- list --json
npm run plugins -- validate --jsonAll commands support --json for machine-readable output. Use npm run plugin or npm run plugins interchangeably.
| Command | Description |
|---|---|
list |
List all plugins with enabled/disabled status |
info <plugin-id> |
Show detailed metadata for a plugin |
validate |
Validate all plugin.json manifests |
deps |
Show aggregated npm/pip/conda dependencies with conflict detection |
install <git-url|path|name> |
Install a plugin repo (git clone, local copy, or registry name) |
uninstall <repo-name> |
Uninstall an installed plugin repo (cannot uninstall core) |
enable <plugin-id> |
Mark a plugin as active in plugin-state.json |
disable <plugin-id> |
Mark a plugin as inactive (cannot disable required plugins) |
enable-all |
Enable all plugins (use --container to scope) |
disable-all |
Disable all non-required plugins (use --container to scope) |
create <type> <Name> |
Scaffold a new plugin (tool, backend, component) |
destroy <plugin-id> |
Delete a plugin (prompts confirmation, --force to skip) |
activate |
Regenerate frontend plugin imports without a full build |
update [repo-name] |
git pull latest for one or all installed repos |
registry add|remove|list |
Manage plugin source URLs |
help |
Show CLI help |
| Flag | Applies to | Description |
|---|---|---|
--json |
all | Output machine-readable JSON. Errors also emit JSON: {"error":"..."} |
--no-color |
all | Disable colored output (also respects NO_COLOR env) |
--link |
install |
Symlink local paths instead of copy (falls back to junction on Windows) |
--container <name> |
create, enable-all, disable-all |
Target container |
--only <names> |
install |
Comma-separated plugin names to keep enabled (disables the rest) |
--force |
destroy |
Skip confirmation prompt |
--tier <tier> |
registry add |
Set tier (core, official, community, private, experimental, deprecated) |
--description <text> |
registry add |
Set description |
--license <spdx> |
registry add |
Set license (e.g. Apache-2.0) |
--author <name> |
registry add |
Set author |
Plugins are identified as <container>/<type>/<name> (e.g. core/tools/Draw). For convenience, the CLI also accepts just <name> and matches the first plugin found.
Frontend UI tools that appear in the MMGIS toolbar. Directory name is plural (tools/) but manifest type is singular ("type": "tool").
Must implement make() and destroy() lifecycle methods.
Required manifest fields: name, paths (mapping of tool name → entry point).
Server-side Express modules. Have plugin.json (metadata) + plugin.js (lifecycle hooks: onceInit, onceStarted, onceSynced). Routes go in routes/, models in models/.
Required manifest fields: name (recommended but optional — backends are keyed by directory name).
UI components loaded into the MMGIS interface. Directory name is plural (components/) but manifest type is singular ("type": "component").
Required manifest fields: name, paths.
npm run plugins -- install https://github.com/org/mmgis-geo-plugins.gitThis clones the repository into plugins/org--mmgis-geo-plugins/. The container directory uses org--repo naming (double-hyphen separator) so that repos with the same name under different organizations don't collide. The repo must follow the standard directory structure with tools/, backend/, and/or components/ subdirectories.
If the argument isn't a URL or existing path, the CLI looks it up in plugin-registries.json by name:
# First, register a source (or use a pre-populated registries file)
npm run plugins -- registry add https://github.com/org/mmgis-geo-plugins.git --tier official
# Then install by name
npm run plugins -- install mmgis-geo-pluginsThis resolves the name to its registered URL and clones/copies from there.
When installing a repo with many plugins but you only need a few:
npm run plugins -- install mmgis-geo-plugins --only SpectralTool,ElevationToolThis installs the full repo (so update works later) but immediately disables all plugins except the ones listed. Disabled plugins are excluded from the build and server — no code or dependencies are bundled.
You can also use disable-all / enable for the same effect after install:
npm run plugins -- install mmgis-geo-plugins
npm run plugins -- disable-all --container mmgis-geo-plugins
npm run plugins -- enable mmgis-geo-plugins/tools/SpectralTool
npm run plugins -- enable mmgis-geo-plugins/tools/ElevationToolnpm run plugins -- install /path/to/my-plugin-repoThis copies the directory into plugins/<dirname>/ (local paths use the directory basename as-is since there is no org to infer). To create a symlink instead (useful during active development so changes are reflected immediately), use the --link flag:
npm run plugins -- install --link /path/to/my-plugin-repoOn Windows, if symlink creation fails due to permissions, --link falls back to a directory junction automatically.
- Frontend plugins are auto-activated by the CLI (regenerates
src/pre/tools.jsandsrc/pre/components.js). In dev mode, webpack-dev-server hot-reloads automatically. For production, runnpm run build. - Restart the server to activate backend plugins.
- Run
npm run plugins:installto install any new npm/pip dependencies declared by the plugins.
The fastest way to create a new plugin is with the create command:
# Create a tool
npm run plugins -- create tool MyTool --container my-plugins
# Create a backend module
npm run plugins -- create backend MyModule --container my-plugins
# Create a component
npm run plugins -- create component MyWidget --container my-plugins
# Container is auto-created if it doesn't exist
npm run plugins -- create tool AnotherTool --container my-pluginsThis scaffolds the directory structure, plugin.json, entry point, CSS, and a test spec. Frontend plugins are auto-activated.
Directory structure (scaffolded by create tool):
plugins/<container>/tools/MyTool/
├── plugin.json
├── MyToolTool.js
├── MyToolTool.css
└── tests/
└── myToolTool.spec.js
plugin.json (minimal required):
{
"name": "MyTool",
"type": "tool",
"defaultIcon": "puzzle-outline",
"description": "Short description of what this tool does.",
"paths": {
"MyToolTool": "./MyToolTool"
}
}MyToolTool.js:
import React from 'react'
import { createRoot } from 'react-dom/client'
import L_ from '@basics/Layers_/Layers_'
import Map_ from '@basics/Map_/Map_'
import './MyToolTool.css'
let MyToolTool = {
height: 0,
width: 300,
_root: null,
make: function () {
const toolPanel = document.getElementById('toolPanel')
if (toolPanel) toolPanel.innerHTML = ''
MyToolTool._root = createRoot(toolPanel)
MyToolTool._root.render(
<div className='myToolTool'>
MyTool
</div>
)
},
destroy: function () {
if (MyToolTool._root) {
MyToolTool._root.unmount()
MyToolTool._root = null
}
},
}
export default MyToolToolDirectory structure:
plugins/<container>/backend/MyModule/
├── plugin.json
├── plugin.js
├── routes/
│ └── myModule.js
└── tests/
└── myModule.spec.js
plugin.json:
{
"name": "MyModule",
"type": "backend",
"version": "1.0.0",
"description": "A custom backend module."
}plugin.js:
const router = require("./routes/myModule");
let setup = {
// Once the app initializes — register routes
onceInit: (s) => {
s.app.use(
s.ROOT_PATH + "/api/mymodule",
s.checkHeadersCodeInjection,
s.setContentType,
router
);
},
// Once the server starts
onceStarted: (s) => {},
// Once all database tables sync
onceSynced: (s) => {},
};
module.exports = setup;Directory structure:
plugins/<container>/components/MyComponent/
├── plugin.json
├── MyComponent.js
├── MyComponent.css
└── tests/
└── myComponent.spec.js
plugin.json:
{
"name": "MyComponent",
"type": "component",
"defaultIcon": "puzzle-outline",
"description": "A custom UI component.",
"paths": {
"MyComponent": "./MyComponent"
}
}Every MMGIS plugin has a plugin.json manifest at the root of its directory. This section describes every recognized field.
Type: string · Required: Yes (all types)
The canonical name of the plugin. Used as the display name in the CLI, configure page, and internal lookups. Should match the directory name by convention.
Type: object — { [entryName: string]: string } · Required: Yes (tools and components only)
Maps entry-point names to their file paths relative to the plugin's own directory. For tools, the key is typically <Name>Tool. For components, it's the component name.
These paths are resolved at build time and written into src/pre/tools.js and src/pre/components.js as webpack imports. Use ./ prefix for paths relative to the plugin directory.
"paths": {
"DrawTool": "./DrawTool"
}Plugins with multiple entry points:
"paths": {
"SightlineTool": "./SightlineTool",
"SightlineTool_Algorithm": "./SightlineTool_Algorithm"
}Type: string — one of "tool", "component", "backend" · Required: No (inferred from directory structure)
Uses singular form even though directory names are plural (tools/, components/).
Type: string
Semantic version. Core plugins use "core" which resolves to the MMGIS application version at runtime.
Type: string · Default: "puzzle-outline" · Applies to: Tools, Components
Icon displayed in the toolbar and configure page. Uses Ionicons icon names.
Type: string
A short human-friendly identifier, typically <container>-<name> in lowercase. Not used for lookups — the runtime ID is computed as <container>/<type>/<name>.
Type: string
Currently informational — not used in any lookup or logic. May become relevant if a plugin marketplace is added.
Type: string
A human-friendly display name, potentially different from name.
Type: string[]
Alternative names for the plugin. Currently informational — may be used in future CLI lookup resolution.
Type: boolean · Default: false
When true, the plugin cannot be disabled (disable) or destroyed (destroy). Used for critical infrastructure plugins (e.g., Users, Config, Accounts). Also implied by overridable: false.
Type: boolean · Default: true
When false: (1) no other plugin can override this one, and (2) the plugin is treated as required — it cannot be disabled or destroyed.
Type: string — one of "core", "community", "private", "official", "experimental", "deprecated"
A classification tag displayed in CLI output.
| Value | Meaning |
|---|---|
core |
Ships with MMGIS |
official |
Maintained by the MMGIS team, distributed separately |
community |
Third-party contributed |
private |
Internal/organization-specific |
experimental |
Unstable, API may change |
deprecated |
Scheduled for removal |
Type: string
A one-line summary. Shown in list --json and info output.
Type: object — { title: string, example: object } · Applies to: Tools, Components
Extended description with a long-form title and an example object showing all available configuration variables.
Standard package metadata. author can be a string or { name, email, url }. license is an SPDX identifier. keywords is a string[].
| Field | Type | Description |
|---|---|---|
toolbarPriority |
number |
Position in toolbar (lower = first). Core tools range ~1001–1020 |
expandable |
boolean |
Whether the tool panel can expand to full width |
separatedTool |
boolean|string |
Renders the tool separately from the main tool panel |
hasVars |
boolean |
Plugin reads per-mission config variables from config.rows |
config |
object |
Defines the configuration UI shown in the configure page. Has rows[] with form field definitions |
kinds |
object |
Sub-types or modes for the tool (used by Kinds tool) |
| Field | Type | Description |
|---|---|---|
priority |
number |
Initialization order (lower = first, default 1000) |
routes |
object |
Informational: { prefix, auth } documenting the API surface |
envs |
object |
Documents environment variables the plugin reads |
Declares runtime dependencies. Aggregated by the deps command and scripts/resolve-plugin-deps.js.
"dependencies": {
"npm": {
"@ffmpeg/ffmpeg": "^0.12.10",
"gifshot": "^0.4.5"
},
"python": {
"pip": ["numpy>=1.21", "scipy"],
"conda": ["gdal>=3.0"]
}
}Array of plugin IDs that this plugin depends on at runtime. Primarily used by tools to declare which backends they need.
Type: string[] · Required: No · Default: []
"pluginDependencies": ["core/backend/Draw", "core/backend/Utils"]The CLI uses this field to:
deps: Show an inter-plugin dependency graph and warn if a dependency is missing or disabled.disable: Warn when disabling a plugin that other enabled plugins depend on.info: Show "Depends on" and "Depended on by" relationships.
Other MMGIS plugins that must be present (version-range checked). The deps command checks for peer warnings.
"peerDependencies": { "core/backend/Draw": ">=1.0.0" }Required runtime versions. Enforced at registration time — if the current MMGIS version does not satisfy the declared range, the plugin is skipped with an error log.
"engines": { "mmgis": ">=5.0.0", "node": ">=22.0.0" }corecontainer is always scanned first.- External containers scanned alphabetically.
- Last-discovered plugin wins when names collide (allows overrides).
- Plugins marked
"overridable": falsein their manifest block overrides. - Disabled plugins in
plugin-state.jsonare skipped during discovery.
- Core plugins (
plugins/core/) cannot be uninstalled, destroyed, or disabled via the CLI. enable-all/disable-allreject--container core.createrejects--container core(core plugins are maintained in the main repo).- Plugins with
"required": trueor"overridable": falsecannot be disabled or destroyed regardless of container.
Core plugins ship with MMGIS and live in plugins/core/. They:
- Cannot be uninstalled or destroyed via the CLI.
- Use
"version": "core"which auto-resolves to the MMGIS version. - Are version-controlled with the main repository.
- Can be overridden by external plugins if
"overridable": true.
plugin-state.json tracks which plugins are enabled or disabled. This file is:
- Gitignored — it is instance-specific configuration.
- Optional — if absent, all installed plugins are enabled by default.
- Required-protected — plugins with
required: trueoroverridable: falseare always enabled regardless of state.
{
"plugins": {
"my-plugins/tools/SpectralTool": { "enabled": true },
"my-plugins/tools/ExperimentalViewer": { "enabled": false }
}
}Disabled plugins are skipped during discovery — they won't be loaded by the build system or the server.
Frontend plugins can use these aliases instead of fragile relative paths:
| Alias | Resolves to |
|---|---|
@basics |
src/essence/Basics/ |
@essence |
src/essence/ |
@design |
src/design-system/ |
@pre |
src/pre/ |
@external |
src/external/ |
Backend plugins use Node.js require() with relative paths — aliases do not apply.
Run npm run plugins -- validate to check all manifests. The validator:
- Errors on missing required fields, wrong types, invalid enum values.
- Warns on unrecognized top-level fields (forward compatibility — the field is preserved).
- Unknown fields do not cause validation failure.
npm run plugins -- validate # Human-readable
npm run plugins -- validate --json # Structured output with per-plugin resultsSee API/pluginValidation.js for the implementation.
plugin-registries.json tracks known plugin sources. When you install a git-based plugin repo, it is automatically added. Local paths are validated on registry add.
npm run plugins -- registry add https://github.com/org/mmgis-plugins.git
npm run plugins -- registry add /local/path/to/plugins
npm run plugins -- registry list
npm run plugins -- registry remove mmgis-plugins
# Add with metadata
npm run plugins -- registry add https://github.com/NASA-AMMOS/mmgis-plugins.git \
--tier official \
--description "Official MMGIS plugin collection" \
--license "Apache-2.0" \
--author "NASA-AMMOS"Registry entries can include optional metadata (tier, description, license, author) which is displayed in registry list and included in --json output. This metadata is set at add-time by the MMGIS administrator, not by the external repo.
Plugin-specific tests live in plugins/<container>/<type>/<Name>/tests/.
Playwright config scans both tests/ and plugins/**/tests/.
npx playwright test plugins/core/tools/Draw/tests/ # Specific plugin
npm run test:unit # All unit tests
npm test # All testsIf you have plugins using the old config.json (tools) or setup.js (backends) format, the discovery system will log a deprecation warning.
Rename config.json to plugin.json and add the recommended fields (version, type, id, author, etc.). The existing fields (name, paths, description, etc.) are unchanged.
- Create a
plugin.jsonwith the backend's metadata (name, version, type, etc.) - Rename
setup.jstoplugin.js— the lifecycle hooks (onceInit,onceStarted,onceSynced) are the same.
Compact reference for agents working with the plugin CLI programmatically.
- All commands support
--json. Error paths also emit JSON when--jsonis set:{"error": "message"}. - The
enablecommand returns{"noop": true, "reason": "required"}for required plugins (exit 0, not an error). - The
typefield in JSON output is always singular (tool,backend,component) — never the plural directory name. list --jsonincludes:id,name,type,container,enabled,core,required,version,tier,author,description,path.info --jsonincludes the fullmanifestobject plus computed fields (enabled,core,required,path).validate --jsonreturns{ valid, total, passed, errors, warnings, results: [{ plugin, valid, errors }] }.activate --jsonreturns{ added: [], removed: [], error }.install --jsonreturns{ command, repo, discovered: [{ id, type, name }], activated: { added, removed } }.- Discovery processes
corefirst, then external containers alphabetically.overridable: falseblocks overrides. plugin-state.jsonis optional. Missing = all enabled.required/overridable: falseplugins ignore state.