spartan-ng-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,259 @@
1
+ # Spartan UI MCP Server
2
+
3
+ An MCP (Model Context Protocol) server that gives AI assistants full access to the [Spartan Angular UI](https://www.spartan.ng) ecosystem — components, blocks, source code, and documentation.
4
+
5
+ ## What It Does
6
+
7
+ - **57 UI Components** with structured API data (Brain & Helm APIs) — selectors, inputs, outputs, models, and code examples
8
+ - **17 Building Blocks** — complete page-level Angular components (sidebar layouts, login/signup forms, calendar interfaces) fetched from GitHub
9
+ - **TypeScript Source Code** — actual component library source from the `spartan-ng/spartan` repository
10
+ - **Canonical Dependency Graph** — real component dependencies from the Spartan CLI
11
+ - **Documentation** — 13 topics including installation, theming, CLI usage, and more
12
+ - **Instant Search** — search across all components by name, selector, directive, or property
13
+
14
+ ## Quick Start
15
+
16
+ Configure your MCP client (Claude Desktop, Cursor, VS Code, etc.):
17
+
18
+ ```json
19
+ {
20
+ "mcpServers": {
21
+ "spartan-ui-mcp": {
22
+ "command": "npx",
23
+ "args": ["spartan-ui-mcp"]
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ ### With GitHub Token (recommended)
30
+
31
+ For block source code and component source fetching, a GitHub token gives you 5000 req/hr instead of 60:
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "spartan-ui-mcp": {
37
+ "command": "npx",
38
+ "args": ["spartan-ui-mcp"],
39
+ "env": {
40
+ "GITHUB_TOKEN": "ghp_your_token_here"
41
+ }
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ No special scopes needed — the token just authenticates against the public repo.
48
+
49
+ <details>
50
+ <summary><strong>How to get a GitHub token</strong></summary>
51
+
52
+ 1. Go to [github.com/settings/tokens](https://github.com/settings/tokens?type=beta)
53
+ 2. Click **"Generate new token"** > **"Fine-grained token"**
54
+ 3. Give it a name (e.g., `spartan-mcp`)
55
+ 4. Set expiration (90 days or custom)
56
+ 5. Under **"Repository access"**, select **"Public Repositories (read-only)"**
57
+ 6. No additional permissions needed — leave everything else as default
58
+ 7. Click **"Generate token"** and copy the `github_pat_...` value
59
+
60
+ Classic tokens also work: create one at [github.com/settings/tokens/new](https://github.com/settings/tokens/new) with no scopes selected.
61
+
62
+ </details>
63
+
64
+ ### Development Setup
65
+
66
+ ```bash
67
+ git clone https://github.com/SOG-web/spartan-ui-mcp.git
68
+ cd spartan-ui-mcp
69
+ npm install
70
+ npm start # or: npm run dev (auto-reload)
71
+ ```
72
+
73
+ ## Tools (17)
74
+
75
+ ### Components
76
+
77
+ | Tool | Description |
78
+ |------|-------------|
79
+ | `spartan_components_list` | List all 57 components with URLs |
80
+ | `spartan_components_get` | Get structured API data (Brain/Helm directives, inputs, outputs, examples). Uses the Spartan Analog API for perfect data quality. |
81
+ | `spartan_components_source` | Fetch actual TypeScript source code from GitHub (`libs/brain/` or `libs/helm/`) |
82
+ | `spartan_components_dependencies` | Get the canonical dependency graph for any component |
83
+
84
+ ### Blocks
85
+
86
+ | Tool | Description |
87
+ |------|-------------|
88
+ | `spartan_blocks_list` | List all building block categories and variants |
89
+ | `spartan_blocks_get` | Fetch complete block source code from GitHub. Returns Angular component files with template, imports, and extracted Spartan/Angular dependencies. |
90
+
91
+ ### Search & Documentation
92
+
93
+ | Tool | Description |
94
+ |------|-------------|
95
+ | `spartan_search` | Instant search across components by name, selector, directive, or input/output property |
96
+ | `spartan_docs_get` | Fetch documentation topics (installation, theming, CLI, dark-mode, etc.) |
97
+ | `spartan_meta` | Get full metadata for autocomplete (all components, blocks, and tool usage) |
98
+
99
+ ### Health & Cache
100
+
101
+ | Tool | Description |
102
+ |------|-------------|
103
+ | `spartan_health_check` | Check spartan.ng page availability |
104
+ | `spartan_health_instructions` | Get Spartan CLI health check instructions |
105
+ | `spartan_health_command` | Build `ng`/`nx` health check commands |
106
+ | `spartan_cache_status` | View cache statistics |
107
+ | `spartan_cache_clear` | Clear cached data |
108
+ | `spartan_cache_rebuild` | Rebuild cache (components, docs, and optionally blocks from GitHub) |
109
+ | `spartan_cache_switch_version` | Switch Spartan UI version for caching |
110
+ | `spartan_cache_list_versions` | List all cached versions |
111
+
112
+ ## Resources
113
+
114
+ MCP resources provide read-only data via URI scheme:
115
+
116
+ - `spartan://components/list` — all components with metadata
117
+ - `spartan://component/{name}/api` — Brain & Helm API specifications
118
+ - `spartan://component/{name}/examples` — code examples
119
+ - `spartan://component/{name}/full` — complete documentation with install snippets
120
+ - `spartan://blocks/list` — all block categories and variants
121
+
122
+ ## Prompts
123
+
124
+ Pre-built conversation templates:
125
+
126
+ - `spartan-get-started` — get started with a component (brain or helm)
127
+ - `spartan-compare-apis` — compare Brain API vs Helm API
128
+ - `spartan-implement-feature` — implement a feature with a component
129
+ - `spartan-troubleshoot` — troubleshoot component issues
130
+ - `spartan-list-components` — list all components by category
131
+ - `spartan-use-block` — use a building block in your project
132
+
133
+ ## Example Usage
134
+
135
+ ```jsonc
136
+ // Get dialog API — returns 7 Brain + 10 Helm directives with full specs
137
+ { "tool": "spartan_components_get", "arguments": { "name": "dialog" } }
138
+
139
+ // Get sidebar source code from GitHub
140
+ { "tool": "spartan_components_source", "arguments": { "name": "sidebar", "layer": "helm" } }
141
+
142
+ // Get a login block with shared utilities
143
+ { "tool": "spartan_blocks_get", "arguments": { "category": "login", "variant": "login-simple-reactive-form", "includeShared": true } }
144
+
145
+ // Search for date-related components
146
+ { "tool": "spartan_search", "arguments": { "query": "date" } }
147
+
148
+ // Get sidebar dependencies (with transitive)
149
+ { "tool": "spartan_components_dependencies", "arguments": { "componentName": "sidebar", "includeTransitive": true } }
150
+ ```
151
+
152
+ ## Architecture
153
+
154
+ ```mermaid
155
+ flowchart TB
156
+ subgraph Client["MCP Client (IDE / AI Assistant)"]
157
+ direction LR
158
+ C1["Tool Calls"]
159
+ C2["Resources"]
160
+ C3["Prompts"]
161
+ end
162
+
163
+ subgraph Server["spartan-ui-mcp (stdio)"]
164
+ direction TB
165
+ Router["server.js — Router"]
166
+
167
+ subgraph Tools["Tool Modules"]
168
+ direction LR
169
+ T1["components.js"]
170
+ T2["blocks.js"]
171
+ T3["search.js"]
172
+ T4["docs.js"]
173
+ T5["analysis.js"]
174
+ end
175
+
176
+ subgraph Cache["Cache Layers"]
177
+ direction LR
178
+ MC["In-Memory\n(5min / 30min / 1hr)"]
179
+ FC["File-Based\n(24hr TTL)\ncache/{version}/"]
180
+ end
181
+ end
182
+
183
+ subgraph Sources["Data Sources"]
184
+ direction LR
185
+ API["Spartan Analog API\n1 request → 57 components\n(selectors, inputs, outputs,\nexamples, install snippets)"]
186
+ GH["GitHub API\nspartan-ng/spartan\n(block source, TS source,\ndependency graph)"]
187
+ WEB["spartan.ng Website\n(documentation pages,\nHTML content)"]
188
+ end
189
+
190
+ Client --> Router
191
+ Router --> Tools
192
+ Tools --> Cache
193
+ Cache --> Sources
194
+
195
+ style Client fill:#1a1a2e,stroke:#e94560,color:#eee
196
+ style Server fill:#16213e,stroke:#0f3460,color:#eee
197
+ style Sources fill:#0f3460,stroke:#533483,color:#eee
198
+ style API fill:#1a472a,stroke:#2d6a4f,color:#eee
199
+ style GH fill:#1a472a,stroke:#2d6a4f,color:#eee
200
+ style WEB fill:#1a472a,stroke:#2d6a4f,color:#eee
201
+ ```
202
+
203
+ ### Request Flow
204
+
205
+ ```
206
+ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────────┐
207
+ │ AI asks: │────▶│ MCP Tool: │────▶│ Cache hit? │────▶│ Return cached │
208
+ │ "How do I │ │ components │ │ Yes ───────▶│ │ structured JSON │
209
+ │ use the │ │ _get │ │ No ────┐ │ └──────────────────┘
210
+ │ sidebar?" │ │ name:sidebar│ └─────────┘ │
211
+ └─────────────┘ └──────────────┘ │ │
212
+ ▼ │
213
+ ┌───────────────────┘
214
+ │ Fetch from source
215
+ ├─ extract=api → Analog API (structured JSON)
216
+ ├─ extract=code → spartan.ng (HTML scraping)
217
+ └─ source tool → GitHub API (TypeScript files)
218
+ ```
219
+
220
+ ## Data Sources
221
+
222
+ The server uses a hybrid approach for maximum data quality:
223
+
224
+ | Source | Used For | Method |
225
+ |--------|----------|--------|
226
+ | Spartan Analog API | Component APIs, examples, install snippets | Single JSON endpoint for all 57 components |
227
+ | GitHub API (`spartan-ng/spartan`) | Block source code, component TypeScript source | Contents API with in-memory caching |
228
+ | spartan.ng website | Documentation pages, HTML content | HTTP fetch with file-based caching |
229
+ | Spartan CLI metadata | Dependency graph (canonical) | Embedded from `primitive-deps.ts` |
230
+
231
+ ## Caching
232
+
233
+ Two layers:
234
+
235
+ 1. **In-memory** — 5min for website content, 30min for Analog API, 1hr for GitHub API
236
+ 2. **File-based** — 24hr TTL under `cache/{version}/` with subdirectories for components, docs, blocks, and source
237
+
238
+ ## Environment Variables
239
+
240
+ | Variable | Default | Description |
241
+ |----------|---------|-------------|
242
+ | `GITHUB_TOKEN` | — | GitHub PAT for higher rate limits (5000/hr vs 60/hr) |
243
+ | `SPARTAN_CACHE_TTL_HOURS` | `24` | File cache TTL in hours |
244
+ | `SPARTAN_CACHE_TTL_MS` | `300000` | In-memory cache TTL in ms |
245
+ | `SPARTAN_FETCH_TIMEOUT_MS` | `15000` | HTTP request timeout in ms |
246
+
247
+ ## Testing
248
+
249
+ ```bash
250
+ node test-e2e.js # 34 end-to-end tests via MCP client protocol
251
+ ```
252
+
253
+ ## License
254
+
255
+ MIT
256
+
257
+ ---
258
+
259
+ Built for the [Spartan Angular UI](https://www.spartan.ng) community.
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "spartan-ng-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server exposing Spartan Angular UI documentation and component tools.",
5
+ "main": "server.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "spartan-ui-mcp": "server.js"
9
+ },
10
+ "files": [
11
+ "server.js",
12
+ "tools/"
13
+ ],
14
+ "scripts": {
15
+ "start": "node server.js",
16
+ "dev": "node --watch server.js",
17
+ "test": "echo \"No tests configured\" && exit 0",
18
+ "prepublish": "echo 'Ready to publish spartan-ui-mcp'",
19
+ "prepare": "husky || true"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "model-context-protocol",
24
+ "spartan",
25
+ "angular",
26
+ "ui",
27
+ "spartan-ui",
28
+ "components",
29
+ "documentation"
30
+ ],
31
+ "author": "Olaleka Raheem <raheemolalekausman84@gmail.com>",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/SOG-web/spartan-ui-mcp.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/SOG-web/spartan-ui-mcp/issues"
39
+ },
40
+ "homepage": "https://github.com/SOG-web/spartan-ui-mcp#readme",
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ },
44
+ "dependencies": {
45
+ "@modelcontextprotocol/sdk": "^1.17.2",
46
+ "zod": "^3.23.8"
47
+ },
48
+ "devDependencies": {
49
+ "@commitlint/cli": "^20.5.0",
50
+ "@commitlint/config-conventional": "^20.5.0",
51
+ "@types/node": "^24.7.1",
52
+ "husky": "9.1.7"
53
+ }
54
+ }
package/server.js ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ //@ts-check
3
+
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { registerComponentTools } from "./tools/components.js";
7
+ import { registerDocsTools } from "./tools/docs.js";
8
+ import { registerHealthTools } from "./tools/health.js";
9
+ import { registerMetaTools } from "./tools/meta.js";
10
+ import { registerSearchTools } from "./tools/search.js";
11
+ import { registerAnalysisTools } from "./tools/analysis.js";
12
+ import { registerBlockTools } from "./tools/blocks.js";
13
+ import { registerResourceHandlers } from "./tools/resources.js";
14
+ import { registerPromptHandlers } from "./tools/prompts.js";
15
+ import { registerCacheTools } from "./tools/cache-tools.js";
16
+
17
+ const server = new McpServer({
18
+ name: "spartan-ui-mcp",
19
+ version: "2.0.0",
20
+ description:
21
+ "MCP server exposing Spartan Angular UI components, blocks, and documentation. " +
22
+ "Provides structured API data, source code from GitHub, and page-level building blocks.",
23
+ });
24
+
25
+ // Register tool modules
26
+ registerComponentTools(server);
27
+ registerDocsTools(server);
28
+ registerHealthTools(server);
29
+ registerMetaTools(server);
30
+ registerSearchTools(server);
31
+ registerAnalysisTools(server);
32
+ registerBlockTools(server);
33
+ registerCacheTools(server);
34
+
35
+ // Register resource handlers
36
+ registerResourceHandlers(server);
37
+
38
+ // Register prompt handlers
39
+ registerPromptHandlers(server);
40
+
41
+ const transport = new StdioServerTransport();
42
+
43
+ await server.connect(transport);
@@ -0,0 +1,286 @@
1
+ //@ts-check
2
+ import { z } from "zod";
3
+ import {
4
+ KNOWN_COMPONENTS,
5
+ COMPONENT_DEPENDENCIES,
6
+ } from "./utils.js";
7
+
8
+ export function registerAnalysisTools(server) {
9
+ // Show component dependencies
10
+ server.registerTool(
11
+ "spartan_components_dependencies",
12
+ {
13
+ title: "Show component dependencies",
14
+ description:
15
+ "Analyze what other components, packages, or dependencies a Spartan UI component requires. " +
16
+ "Includes Angular CDK dependencies, peer components, and installation requirements.",
17
+ inputSchema: {
18
+ componentName: z
19
+ .string()
20
+ .min(1, "componentName is required")
21
+ .describe("Spartan component name (e.g., 'calendar', 'dialog')"),
22
+ includeTransitive: z
23
+ .boolean()
24
+ .default(false)
25
+ .describe(
26
+ "Include transitive dependencies (dependencies of dependencies)"
27
+ ),
28
+ },
29
+ },
30
+ async (args) => {
31
+ const componentName = String(args.componentName || "")
32
+ .trim()
33
+ .toLowerCase();
34
+ if (!KNOWN_COMPONENTS.includes(componentName)) {
35
+ throw new Error(
36
+ `Unknown component: ${componentName}. Available: ${KNOWN_COMPONENTS.join(
37
+ ", "
38
+ )}`
39
+ );
40
+ }
41
+
42
+ const dependencies = await analyzeComponentDependencies(
43
+ componentName,
44
+ args.includeTransitive
45
+ );
46
+
47
+ return {
48
+ content: [
49
+ {
50
+ type: "text",
51
+ text: JSON.stringify(
52
+ {
53
+ component: componentName,
54
+ dependencies,
55
+ },
56
+ null,
57
+ 2
58
+ ),
59
+ },
60
+ ],
61
+ };
62
+ }
63
+ );
64
+
65
+ // Find related/similar components
66
+ // COMMENTED OUT: Not producing useful results
67
+ /*
68
+ server.registerTool(
69
+ "spartan_components_related",
70
+ {
71
+ title: "Find related or similar components",
72
+ description:
73
+ "Find Spartan UI components that are related to or similar to a given component. " +
74
+ "Analyzes functionality, use cases, and API patterns to suggest alternatives and complementary components.",
75
+ inputSchema: {
76
+ componentName: z
77
+ .string()
78
+ .min(1, "componentName is required")
79
+ .describe("Spartan component name to find related components for"),
80
+ relationshipType: z
81
+ .enum(["similar", "complementary", "alternative", "all"])
82
+ .default("all")
83
+ .describe(
84
+ "Type of relationship: 'similar' (same use case), 'complementary' (work together), 'alternative' (different approach), 'all'"
85
+ ),
86
+ limit: z
87
+ .number()
88
+ .min(1)
89
+ .max(10)
90
+ .default(5)
91
+ .describe("Maximum number of related components to return"),
92
+ },
93
+ },
94
+ async (args) => {
95
+ const componentName = String(args.componentName || "")
96
+ .trim()
97
+ .toLowerCase();
98
+ if (!KNOWN_COMPONENTS.includes(componentName)) {
99
+ throw new Error(`Unknown component: ${componentName}`);
100
+ }
101
+
102
+ const related = await findRelatedComponents(
103
+ componentName,
104
+ args.relationshipType,
105
+ args.limit
106
+ );
107
+
108
+ return {
109
+ content: [
110
+ {
111
+ type: "text",
112
+ text: JSON.stringify(
113
+ {
114
+ component: componentName,
115
+ relationshipType: args.relationshipType,
116
+ relatedComponents: related,
117
+ processingInstructions:
118
+ "Present related components with explanations of relationships, use case comparisons, and when to choose each option.",
119
+ },
120
+ null,
121
+ 2
122
+ ),
123
+ },
124
+ ],
125
+ };
126
+ }
127
+ );
128
+ */
129
+
130
+ // List component variants (Brain vs Helm API)
131
+ // COMMENTED OUT: Not producing useful results
132
+ /*
133
+ server.registerTool(
134
+ "spartan_components_variants",
135
+ {
136
+ title: "List component variants (Brain vs Helm API)",
137
+ description:
138
+ "Compare Brain API (low-level, unstyled) and Helm API (high-level, styled) variants of a component. " +
139
+ "Shows differences in API, styling approach, and when to use each variant.",
140
+ inputSchema: {
141
+ componentName: z
142
+ .string()
143
+ .min(1, "componentName is required")
144
+ .describe("Spartan component name"),
145
+ includeComparison: z
146
+ .boolean()
147
+ .default(true)
148
+ .describe("Include detailed comparison between variants"),
149
+ },
150
+ },
151
+ async (args) => {
152
+ const componentName = String(args.componentName || "")
153
+ .trim()
154
+ .toLowerCase();
155
+ if (!KNOWN_COMPONENTS.includes(componentName)) {
156
+ throw new Error(`Unknown component: ${componentName}`);
157
+ }
158
+
159
+ const variants = await analyzeComponentVariants(
160
+ componentName,
161
+ args.includeComparison
162
+ );
163
+
164
+ return {
165
+ content: [
166
+ {
167
+ type: "text",
168
+ text: JSON.stringify(
169
+ {
170
+ component: componentName,
171
+ variants,
172
+ processingInstructions:
173
+ "Present variants with clear explanations of differences, use cases, and migration guidance between Brain and Helm APIs.",
174
+ },
175
+ null,
176
+ 2
177
+ ),
178
+ },
179
+ ],
180
+ };
181
+ }
182
+ );
183
+ */
184
+
185
+ // Accessibility check — removed: produced fake scores based on string matching,
186
+ // not real accessibility analysis. The data was misleading.
187
+ /* REMOVED in v2.0
188
+ server.registerTool(
189
+ "spartan_accessibility_check_REMOVED",
190
+ {
191
+ title: "Check component accessibility features",
192
+ description:
193
+ "Analyze accessibility features, ARIA support, keyboard navigation, and screen reader compatibility " +
194
+ "for a Spartan UI component. Provides accessibility best practices and implementation guidance.",
195
+ inputSchema: {
196
+ componentName: z
197
+ .string()
198
+ .min(1, "componentName is required")
199
+ .describe("Spartan component name"),
200
+ checkType: z
201
+ .enum(["overview", "aria", "keyboard", "screenreader", "wcag", "all"])
202
+ .default("all")
203
+ .describe(
204
+ "Type of accessibility check: specific area or 'all' for comprehensive analysis"
205
+ ),
206
+ },
207
+ },
208
+ async (args) => {
209
+ const componentName = String(args.componentName || "")
210
+ .trim()
211
+ .toLowerCase();
212
+ if (!KNOWN_COMPONENTS.includes(componentName)) {
213
+ throw new Error(`Unknown component: ${componentName}`);
214
+ }
215
+
216
+ const accessibility = await analyzeAccessibility(
217
+ componentName,
218
+ args.checkType
219
+ );
220
+
221
+ return {
222
+ content: [
223
+ {
224
+ type: "text",
225
+ text: JSON.stringify(
226
+ {
227
+ component: componentName,
228
+ checkType: args.checkType,
229
+ accessibility,
230
+ },
231
+ null,
232
+ 2
233
+ ),
234
+ },
235
+ ],
236
+ };
237
+ }
238
+ );
239
+ REMOVED in v2.0 */
240
+ }
241
+
242
+ /**
243
+ * Analyze component dependencies
244
+ * @param {string} componentName
245
+ * @param {boolean} includeTransitive
246
+ */
247
+ async function analyzeComponentDependencies(componentName, includeTransitive) {
248
+ // Use the canonical dependency graph from the Spartan CLI
249
+ const directDeps = COMPONENT_DEPENDENCIES[componentName] || [];
250
+
251
+ const dependencies = {
252
+ direct: directDeps.filter((d) => d !== "utils"),
253
+ installCommand: `npx ng g @spartan-ng/cli:ui ${componentName}`,
254
+ allRequired: directDeps,
255
+ };
256
+
257
+ // Add transitive dependencies if requested
258
+ if (includeTransitive) {
259
+ const transitive = new Set();
260
+ const visited = new Set([componentName]);
261
+
262
+ const collectDeps = (name) => {
263
+ const deps = COMPONENT_DEPENDENCIES[name] || [];
264
+ for (const dep of deps) {
265
+ if (!visited.has(dep)) {
266
+ visited.add(dep);
267
+ if (dep !== "utils") transitive.add(dep);
268
+ collectDeps(dep);
269
+ }
270
+ }
271
+ };
272
+ collectDeps(componentName);
273
+
274
+ // Remove direct deps from transitive
275
+ for (const d of directDeps) transitive.delete(d);
276
+ dependencies.transitive = [...transitive];
277
+ }
278
+
279
+ return dependencies;
280
+ }
281
+
282
+ // Remaining helper functions removed in v2.0:
283
+ // - findRelatedComponents, analyzeComponentVariants, analyzeAccessibility
284
+ // - All helper functions for the above (getSimilarComponents, etc.)
285
+ // These were producing fake/hardcoded data. The dependency graph now comes
286
+ // from COMPONENT_DEPENDENCIES (canonical source: spartan-ng/cli).