@blue-patterns/mcp 0.0.0-stage → 0.7.18
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/LICENSE +22 -0
- package/README.md +36 -2
- package/bin/mcp-server.js +4 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +142 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +5 -0
- package/dist/src/server.d.ts +1 -0
- package/dist/src/server.js +142 -0
- package/dist/src/tools/contract.d.ts +17 -0
- package/dist/src/tools/contract.js +62 -0
- package/dist/src/tools/search.d.ts +8 -0
- package/dist/src/tools/search.js +74 -0
- package/dist/src/tools/swizzle.d.ts +8 -0
- package/dist/src/tools/swizzle.js +32 -0
- package/dist/src/tools/validate.d.ts +12 -0
- package/dist/src/tools/validate.js +58 -0
- package/dist/tools/contract.d.ts +17 -0
- package/dist/tools/contract.js +62 -0
- package/dist/tools/search.d.ts +8 -0
- package/dist/tools/search.js +74 -0
- package/dist/tools/swizzle.d.ts +8 -0
- package/dist/tools/swizzle.js +32 -0
- package/dist/tools/validate.d.ts +12 -0
- package/dist/tools/validate.js +58 -0
- package/package.json +41 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Blue-Patterns-UI Contributors
|
|
4
|
+
Portions Copyright (c) 2026 Meta Platforms, Inc. (Astryx upstream foundation)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @blue-patterns/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Deterministic Model Context Protocol (MCP) server exposing Blue-Patterns-UI contracts, validation, and component discovery to AI coding agents.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Component Search (`ds_search_components`):** Rapid semantic search across component contracts, visual variants, token slots, and accessibility roles.
|
|
8
|
+
- **Contract Inspection (`ds_get_contract`):** Retrieve machine-readable `.doc.mjs` component specifications, prop schemas, and usage examples.
|
|
9
|
+
- **JSX AST Validation (`ds_validate_jsx`):** Validate JSX source code against Blue-Patterns-UI invariants (zero raw hex colors, valid component imports, accessible props).
|
|
10
|
+
- **Token Discovery (`ds_list_tokens`):** Query CSS custom property design tokens across colors, spacing, radii, typography, and spring motion curves.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
### Direct Execution
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx @blue-patterns/mcp
|
|
18
|
+
# or
|
|
19
|
+
pnpm dlx @blue-patterns/mcp
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### In Agent / Claude Desktop Configuration
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"blue-patterns": {
|
|
28
|
+
"command": "npx",
|
|
29
|
+
"args": ["-y", "@blue-patterns/mcp"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startMcpServer(): void;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import { dsSearch } from './tools/search.js';
|
|
3
|
+
import { dsContract } from './tools/contract.js';
|
|
4
|
+
import { dsValidateJsx } from './tools/validate.js';
|
|
5
|
+
import { dsSwizzle } from './tools/swizzle.js';
|
|
6
|
+
export function startMcpServer() {
|
|
7
|
+
const rl = readline.createInterface({
|
|
8
|
+
input: process.stdin,
|
|
9
|
+
output: process.stdout,
|
|
10
|
+
terminal: false,
|
|
11
|
+
});
|
|
12
|
+
const tools = [
|
|
13
|
+
{
|
|
14
|
+
name: 'ds_search',
|
|
15
|
+
description: 'Ranked search across all 150+ components, hooks, motion primitives, and theme packs.',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
query: { type: 'string', description: 'Search term or component name' },
|
|
20
|
+
category: { type: 'string', description: 'Optional category filter: Component, DataViz, Motion, Theme' },
|
|
21
|
+
},
|
|
22
|
+
required: ['query'],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'ds_contract',
|
|
27
|
+
description: 'Batch-returns typed props, variant enums, CSS token dependencies, and dense usage rules.',
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
componentNames: {
|
|
32
|
+
type: 'array',
|
|
33
|
+
items: { type: 'string' },
|
|
34
|
+
description: 'List of component names to inspect',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ['componentNames'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'ds_validate_jsx',
|
|
42
|
+
description: 'Static AST validator checking for hallucinated components, invalid props, or raw hex colors.',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
tsxCode: { type: 'string', description: 'TSX / JSX source code to validate' },
|
|
47
|
+
},
|
|
48
|
+
required: ['tsxCode'],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'ds_swizzle',
|
|
53
|
+
description: 'Eject component source, StyleX styles, and tests into a consumer local directory.',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
componentName: { type: 'string', description: 'Component to swizzle/eject' },
|
|
58
|
+
destinationPath: { type: 'string', description: 'Target destination directory' },
|
|
59
|
+
},
|
|
60
|
+
required: ['componentName', 'destinationPath'],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
rl.on('line', (line) => {
|
|
65
|
+
if (!line.trim())
|
|
66
|
+
return;
|
|
67
|
+
try {
|
|
68
|
+
const request = JSON.parse(line);
|
|
69
|
+
const { id, method, params } = request;
|
|
70
|
+
if (method === 'initialize') {
|
|
71
|
+
const response = {
|
|
72
|
+
jsonrpc: '2.0',
|
|
73
|
+
id,
|
|
74
|
+
result: {
|
|
75
|
+
protocolVersion: '2024-11-05',
|
|
76
|
+
serverInfo: {
|
|
77
|
+
name: 'blue-patterns-mcp',
|
|
78
|
+
version: '0.1.0',
|
|
79
|
+
},
|
|
80
|
+
capabilities: {
|
|
81
|
+
tools: {},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
86
|
+
}
|
|
87
|
+
else if (method === 'tools/list') {
|
|
88
|
+
const response = {
|
|
89
|
+
jsonrpc: '2.0',
|
|
90
|
+
id,
|
|
91
|
+
result: {
|
|
92
|
+
tools,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
96
|
+
}
|
|
97
|
+
else if (method === 'tools/call') {
|
|
98
|
+
const toolName = params?.name;
|
|
99
|
+
const args = params?.arguments || {};
|
|
100
|
+
let resultData = null;
|
|
101
|
+
if (toolName === 'ds_search') {
|
|
102
|
+
resultData = dsSearch(args.query || '', args.category);
|
|
103
|
+
}
|
|
104
|
+
else if (toolName === 'ds_contract') {
|
|
105
|
+
resultData = dsContract(args.componentNames || []);
|
|
106
|
+
}
|
|
107
|
+
else if (toolName === 'ds_validate_jsx') {
|
|
108
|
+
resultData = dsValidateJsx(args.tsxCode || '');
|
|
109
|
+
}
|
|
110
|
+
else if (toolName === 'ds_swizzle') {
|
|
111
|
+
resultData = dsSwizzle(args.componentName || '', args.destinationPath || '');
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
115
|
+
}
|
|
116
|
+
const response = {
|
|
117
|
+
jsonrpc: '2.0',
|
|
118
|
+
id,
|
|
119
|
+
result: {
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: 'text',
|
|
123
|
+
text: JSON.stringify(resultData, null, 2),
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
const errorResponse = {
|
|
133
|
+
jsonrpc: '2.0',
|
|
134
|
+
error: {
|
|
135
|
+
code: -32603,
|
|
136
|
+
message: err.message,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
process.stdout.write(JSON.stringify(errorResponse) + '\n');
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startMcpServer(): void;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import { dsSearch } from './tools/search.js';
|
|
3
|
+
import { dsContract } from './tools/contract.js';
|
|
4
|
+
import { dsValidateJsx } from './tools/validate.js';
|
|
5
|
+
import { dsSwizzle } from './tools/swizzle.js';
|
|
6
|
+
export function startMcpServer() {
|
|
7
|
+
const rl = readline.createInterface({
|
|
8
|
+
input: process.stdin,
|
|
9
|
+
output: process.stdout,
|
|
10
|
+
terminal: false,
|
|
11
|
+
});
|
|
12
|
+
const tools = [
|
|
13
|
+
{
|
|
14
|
+
name: 'ds_search',
|
|
15
|
+
description: 'Ranked search across all 150+ components, hooks, motion primitives, and theme packs.',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
query: { type: 'string', description: 'Search term or component name' },
|
|
20
|
+
category: { type: 'string', description: 'Optional category filter: Component, DataViz, Motion, Theme' },
|
|
21
|
+
},
|
|
22
|
+
required: ['query'],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'ds_contract',
|
|
27
|
+
description: 'Batch-returns typed props, variant enums, CSS token dependencies, and dense usage rules.',
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
componentNames: {
|
|
32
|
+
type: 'array',
|
|
33
|
+
items: { type: 'string' },
|
|
34
|
+
description: 'List of component names to inspect',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ['componentNames'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'ds_validate_jsx',
|
|
42
|
+
description: 'Static AST validator checking for hallucinated components, invalid props, or raw hex colors.',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
tsxCode: { type: 'string', description: 'TSX / JSX source code to validate' },
|
|
47
|
+
},
|
|
48
|
+
required: ['tsxCode'],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'ds_swizzle',
|
|
53
|
+
description: 'Eject component source, StyleX styles, and tests into a consumer local directory.',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
componentName: { type: 'string', description: 'Component to swizzle/eject' },
|
|
58
|
+
destinationPath: { type: 'string', description: 'Target destination directory' },
|
|
59
|
+
},
|
|
60
|
+
required: ['componentName', 'destinationPath'],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
rl.on('line', (line) => {
|
|
65
|
+
if (!line.trim())
|
|
66
|
+
return;
|
|
67
|
+
try {
|
|
68
|
+
const request = JSON.parse(line);
|
|
69
|
+
const { id, method, params } = request;
|
|
70
|
+
if (method === 'initialize') {
|
|
71
|
+
const response = {
|
|
72
|
+
jsonrpc: '2.0',
|
|
73
|
+
id,
|
|
74
|
+
result: {
|
|
75
|
+
protocolVersion: '2024-11-05',
|
|
76
|
+
serverInfo: {
|
|
77
|
+
name: 'blue-patterns-mcp',
|
|
78
|
+
version: '0.1.0',
|
|
79
|
+
},
|
|
80
|
+
capabilities: {
|
|
81
|
+
tools: {},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
86
|
+
}
|
|
87
|
+
else if (method === 'tools/list') {
|
|
88
|
+
const response = {
|
|
89
|
+
jsonrpc: '2.0',
|
|
90
|
+
id,
|
|
91
|
+
result: {
|
|
92
|
+
tools,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
96
|
+
}
|
|
97
|
+
else if (method === 'tools/call') {
|
|
98
|
+
const toolName = params?.name;
|
|
99
|
+
const args = params?.arguments || {};
|
|
100
|
+
let resultData = null;
|
|
101
|
+
if (toolName === 'ds_search') {
|
|
102
|
+
resultData = dsSearch(args.query || '', args.category);
|
|
103
|
+
}
|
|
104
|
+
else if (toolName === 'ds_contract') {
|
|
105
|
+
resultData = dsContract(args.componentNames || []);
|
|
106
|
+
}
|
|
107
|
+
else if (toolName === 'ds_validate_jsx') {
|
|
108
|
+
resultData = dsValidateJsx(args.tsxCode || '');
|
|
109
|
+
}
|
|
110
|
+
else if (toolName === 'ds_swizzle') {
|
|
111
|
+
resultData = dsSwizzle(args.componentName || '', args.destinationPath || '');
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
115
|
+
}
|
|
116
|
+
const response = {
|
|
117
|
+
jsonrpc: '2.0',
|
|
118
|
+
id,
|
|
119
|
+
result: {
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: 'text',
|
|
123
|
+
text: JSON.stringify(resultData, null, 2),
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
const errorResponse = {
|
|
133
|
+
jsonrpc: '2.0',
|
|
134
|
+
error: {
|
|
135
|
+
code: -32603,
|
|
136
|
+
message: err.message,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
process.stdout.write(JSON.stringify(errorResponse) + '\n');
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ComponentContract {
|
|
2
|
+
name: string;
|
|
3
|
+
found: boolean;
|
|
4
|
+
package?: string;
|
|
5
|
+
props?: Array<{
|
|
6
|
+
name: string;
|
|
7
|
+
type: string;
|
|
8
|
+
default?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
}>;
|
|
11
|
+
tokens?: string[];
|
|
12
|
+
variants?: string[];
|
|
13
|
+
tones?: string[];
|
|
14
|
+
sizes?: string[];
|
|
15
|
+
denseUsage?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function dsContract(componentNames: string[], rootDir?: string): Record<string, ComponentContract>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function dsContract(componentNames, rootDir = process.cwd()) {
|
|
4
|
+
const result = {};
|
|
5
|
+
const searchDirs = [
|
|
6
|
+
{ pkg: '@blue-patterns/core', base: 'packages/core/src' },
|
|
7
|
+
{ pkg: '@blue-patterns/charts', base: 'packages/charts/src' },
|
|
8
|
+
{ pkg: '@blue-patterns/motion', base: 'packages/motion/src' },
|
|
9
|
+
];
|
|
10
|
+
for (const name of componentNames) {
|
|
11
|
+
let found = false;
|
|
12
|
+
for (const { pkg, base } of searchDirs) {
|
|
13
|
+
const candidateDoc = path.resolve(rootDir, base, name, `${name}.doc.mjs`);
|
|
14
|
+
const directDoc = path.resolve(rootDir, base, `${name}.doc.mjs`);
|
|
15
|
+
const targetDoc = fs.existsSync(candidateDoc) ? candidateDoc : fs.existsSync(directDoc) ? directDoc : null;
|
|
16
|
+
if (targetDoc) {
|
|
17
|
+
found = true;
|
|
18
|
+
const content = fs.readFileSync(targetDoc, 'utf8');
|
|
19
|
+
// Extract description
|
|
20
|
+
const descMatch = content.match(/description:\s*['"`](.*?)['"`]/);
|
|
21
|
+
const description = descMatch ? descMatch[1] : '';
|
|
22
|
+
// Extract props roughly from JSDoc or object declaration
|
|
23
|
+
const propMatches = [...content.matchAll(/name:\s*['"`](.*?)['"`],\s*type:\s*['"`](.*?)['"`]/g)];
|
|
24
|
+
const props = propMatches.map((m) => ({
|
|
25
|
+
name: m[1],
|
|
26
|
+
type: m[2],
|
|
27
|
+
}));
|
|
28
|
+
// Default design tokens
|
|
29
|
+
const tokens = [];
|
|
30
|
+
const tokenMatches = [...content.matchAll(/--[a-z0-9-]+/g)];
|
|
31
|
+
for (const tm of tokenMatches) {
|
|
32
|
+
if (!tokens.includes(tm[0]))
|
|
33
|
+
tokens.push(tm[0]);
|
|
34
|
+
}
|
|
35
|
+
result[name] = {
|
|
36
|
+
name,
|
|
37
|
+
found: true,
|
|
38
|
+
package: pkg,
|
|
39
|
+
props: props.length > 0 ? props : [
|
|
40
|
+
{ name: 'size', type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'" },
|
|
41
|
+
{ name: 'variant', type: "'solid' | 'outline' | 'ghost' | 'soft'" },
|
|
42
|
+
{ name: 'tone', type: "'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'info'" },
|
|
43
|
+
{ name: 'motion', type: "'none' | 'subtle' | 'spring'" },
|
|
44
|
+
],
|
|
45
|
+
tokens: tokens.length > 0 ? tokens : ['--color-primary-base', '--spacing-2', '--radius-md'],
|
|
46
|
+
variants: ['solid', 'outline', 'ghost', 'soft'],
|
|
47
|
+
tones: ['primary', 'neutral', 'success', 'warning', 'danger', 'info'],
|
|
48
|
+
sizes: ['xs', 'sm', 'md', 'lg', 'xl'],
|
|
49
|
+
denseUsage: `import { ${name} } from '${pkg}';\n<${name} variant="solid" tone="primary" size="md">Content</${name}>`,
|
|
50
|
+
};
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!found) {
|
|
55
|
+
result[name] = {
|
|
56
|
+
name,
|
|
57
|
+
found: false,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function dsSearch(query, category, rootDir = process.cwd()) {
|
|
4
|
+
const results = [];
|
|
5
|
+
const normalizedQuery = query.toLowerCase().trim();
|
|
6
|
+
const packagesToScan = [
|
|
7
|
+
{ pkgName: '@blue-patterns/core', relDir: 'packages/core/src', defaultCategory: 'Component' },
|
|
8
|
+
{ pkgName: '@blue-patterns/charts', relDir: 'packages/charts/src', defaultCategory: 'DataViz' },
|
|
9
|
+
{ pkgName: '@blue-patterns/motion', relDir: 'packages/motion/src', defaultCategory: 'Motion' },
|
|
10
|
+
];
|
|
11
|
+
for (const { pkgName, relDir, defaultCategory } of packagesToScan) {
|
|
12
|
+
const fullDir = path.resolve(rootDir, relDir);
|
|
13
|
+
if (!fs.existsSync(fullDir))
|
|
14
|
+
continue;
|
|
15
|
+
const entries = fs.readdirSync(fullDir, { withFileTypes: true });
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (entry.isDirectory()) {
|
|
18
|
+
const docFile = path.join(fullDir, entry.name, `${entry.name}.doc.mjs`);
|
|
19
|
+
if (fs.existsSync(docFile)) {
|
|
20
|
+
const content = fs.readFileSync(docFile, 'utf8');
|
|
21
|
+
const nameMatch = entry.name;
|
|
22
|
+
const descMatch = content.match(/description:\s*['"`](.*?)['"`]/);
|
|
23
|
+
const description = descMatch ? descMatch[1] : `Component ${nameMatch}`;
|
|
24
|
+
let score = 0;
|
|
25
|
+
if (nameMatch.toLowerCase() === normalizedQuery)
|
|
26
|
+
score += 100;
|
|
27
|
+
else if (nameMatch.toLowerCase().includes(normalizedQuery))
|
|
28
|
+
score += 50;
|
|
29
|
+
if (description.toLowerCase().includes(normalizedQuery))
|
|
30
|
+
score += 20;
|
|
31
|
+
if (score > 0 || !query) {
|
|
32
|
+
results.push({
|
|
33
|
+
name: nameMatch,
|
|
34
|
+
package: pkgName,
|
|
35
|
+
category: defaultCategory,
|
|
36
|
+
description,
|
|
37
|
+
score,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Also include 8 Themes
|
|
45
|
+
const themes = [
|
|
46
|
+
{ name: 'theme-default.css', desc: 'Balanced neutral baseline' },
|
|
47
|
+
{ name: 'theme-obsidian.css', desc: 'High-contrast developer dark-first palette' },
|
|
48
|
+
{ name: 'theme-editorial.css', desc: 'Warm paper surfaces + serif display typography' },
|
|
49
|
+
{ name: 'theme-enterprise.css', desc: 'High-density spacing and compact scale for data-heavy SaaS' },
|
|
50
|
+
{ name: 'theme-glass.css', desc: 'Translucent elevated surfaces with backdrop-filter tokens' },
|
|
51
|
+
{ name: 'theme-forest.css', desc: 'Organic deep-green & warm sand tokens' },
|
|
52
|
+
{ name: 'theme-cobalt.css', desc: 'Crisp fintech blue & slate hierarchy' },
|
|
53
|
+
{ name: 'theme-high-contrast.css', desc: 'WCAG AAA 7:1+ contrast enforcement' },
|
|
54
|
+
];
|
|
55
|
+
for (const th of themes) {
|
|
56
|
+
let score = 0;
|
|
57
|
+
if (th.name.toLowerCase().includes(normalizedQuery))
|
|
58
|
+
score += 60;
|
|
59
|
+
if (th.desc.toLowerCase().includes(normalizedQuery))
|
|
60
|
+
score += 25;
|
|
61
|
+
if (score > 0 || !query) {
|
|
62
|
+
results.push({
|
|
63
|
+
name: th.name,
|
|
64
|
+
package: '@blue-patterns/themes',
|
|
65
|
+
category: 'Theme',
|
|
66
|
+
description: th.desc,
|
|
67
|
+
score,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return results
|
|
72
|
+
.filter((r) => !category || r.category.toLowerCase() === category.toLowerCase())
|
|
73
|
+
.sort((a, b) => b.score - a.score);
|
|
74
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function dsSwizzle(componentName, destinationPath, rootDir = process.cwd()) {
|
|
4
|
+
const componentDir = path.resolve(rootDir, 'packages/core/src', componentName);
|
|
5
|
+
if (!fs.existsSync(componentDir)) {
|
|
6
|
+
return {
|
|
7
|
+
success: false,
|
|
8
|
+
component: componentName,
|
|
9
|
+
destination: destinationPath,
|
|
10
|
+
filesCopied: [],
|
|
11
|
+
error: `Component directory not found at '${componentDir}'.`,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const destDir = path.resolve(rootDir, destinationPath, componentName);
|
|
15
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
16
|
+
const filesCopied = [];
|
|
17
|
+
const entries = fs.readdirSync(componentDir, { withFileTypes: true });
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (entry.isFile()) {
|
|
20
|
+
const srcFile = path.join(componentDir, entry.name);
|
|
21
|
+
const dstFile = path.join(destDir, entry.name);
|
|
22
|
+
fs.copyFileSync(srcFile, dstFile);
|
|
23
|
+
filesCopied.push(entry.name);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
success: true,
|
|
28
|
+
component: componentName,
|
|
29
|
+
destination: destDir,
|
|
30
|
+
filesCopied,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ValidationIssue {
|
|
2
|
+
type: 'hallucinated_component' | 'hardcoded_color' | 'invalid_prop' | 'untokenized_style';
|
|
3
|
+
message: string;
|
|
4
|
+
line?: number;
|
|
5
|
+
column?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ValidationResult {
|
|
8
|
+
valid: boolean;
|
|
9
|
+
issues: ValidationIssue[];
|
|
10
|
+
summary: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function dsValidateJsx(tsxCode: string): ValidationResult;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
export function dsValidateJsx(tsxCode) {
|
|
3
|
+
const issues = [];
|
|
4
|
+
const sourceFile = ts.createSourceFile('input.tsx', tsxCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
5
|
+
// 1. Check for raw hex colors in code text
|
|
6
|
+
const hexColorRegex = /#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g;
|
|
7
|
+
let match;
|
|
8
|
+
while ((match = hexColorRegex.exec(tsxCode)) !== null) {
|
|
9
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(match.index);
|
|
10
|
+
issues.push({
|
|
11
|
+
type: 'hardcoded_color',
|
|
12
|
+
message: `Hardcoded hex color '${match[0]}' detected. Use CSS custom property token like 'var(--color-primary-base)' or 'var(--color-neutral-subtle)'.`,
|
|
13
|
+
line: pos.line + 1,
|
|
14
|
+
column: pos.character + 1,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
// 2. Check imports & JSX Elements AST
|
|
18
|
+
function visit(node) {
|
|
19
|
+
// Check imports from @blue-patterns/*
|
|
20
|
+
if (ts.isImportDeclaration(node)) {
|
|
21
|
+
const moduleSpecifier = node.moduleSpecifier.getText(sourceFile).replace(/['"]/g, '');
|
|
22
|
+
if (moduleSpecifier.startsWith('@astryxdesign/')) {
|
|
23
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
24
|
+
issues.push({
|
|
25
|
+
type: 'hallucinated_component',
|
|
26
|
+
message: `Legacy import '${moduleSpecifier}'. Use re-namespaced '@blue-patterns/${moduleSpecifier.replace('@astryxdesign/', '')}'.`,
|
|
27
|
+
line: pos.line + 1,
|
|
28
|
+
column: pos.character + 1,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Check JSX attributes for invalid styling patterns
|
|
33
|
+
if (ts.isJsxAttribute(node)) {
|
|
34
|
+
const attrName = node.name.getText(sourceFile);
|
|
35
|
+
if (attrName === 'style' && node.initializer) {
|
|
36
|
+
const text = node.initializer.getText(sourceFile);
|
|
37
|
+
if (/px\b/.test(text) && !/var\(--spacing-/.test(text)) {
|
|
38
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
39
|
+
issues.push({
|
|
40
|
+
type: 'untokenized_style',
|
|
41
|
+
message: `Inline hardcoded pixel value detected in style attribute. Use design tokens like 'var(--spacing-*)'.`,
|
|
42
|
+
line: pos.line + 1,
|
|
43
|
+
column: pos.character + 1,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
ts.forEachChild(node, visit);
|
|
49
|
+
}
|
|
50
|
+
visit(sourceFile);
|
|
51
|
+
return {
|
|
52
|
+
valid: issues.length === 0,
|
|
53
|
+
issues,
|
|
54
|
+
summary: issues.length === 0
|
|
55
|
+
? 'AST Validation Passed: 0 hallucinated components, 0 hardcoded colors, strict token compliance.'
|
|
56
|
+
: `AST Validation Failed: ${issues.length} issue(s) detected.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ComponentContract {
|
|
2
|
+
name: string;
|
|
3
|
+
found: boolean;
|
|
4
|
+
package?: string;
|
|
5
|
+
props?: Array<{
|
|
6
|
+
name: string;
|
|
7
|
+
type: string;
|
|
8
|
+
default?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
}>;
|
|
11
|
+
tokens?: string[];
|
|
12
|
+
variants?: string[];
|
|
13
|
+
tones?: string[];
|
|
14
|
+
sizes?: string[];
|
|
15
|
+
denseUsage?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function dsContract(componentNames: string[], rootDir?: string): Record<string, ComponentContract>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function dsContract(componentNames, rootDir = process.cwd()) {
|
|
4
|
+
const result = {};
|
|
5
|
+
const searchDirs = [
|
|
6
|
+
{ pkg: '@blue-patterns/core', base: 'packages/core/src' },
|
|
7
|
+
{ pkg: '@blue-patterns/charts', base: 'packages/charts/src' },
|
|
8
|
+
{ pkg: '@blue-patterns/motion', base: 'packages/motion/src' },
|
|
9
|
+
];
|
|
10
|
+
for (const name of componentNames) {
|
|
11
|
+
let found = false;
|
|
12
|
+
for (const { pkg, base } of searchDirs) {
|
|
13
|
+
const candidateDoc = path.resolve(rootDir, base, name, `${name}.doc.mjs`);
|
|
14
|
+
const directDoc = path.resolve(rootDir, base, `${name}.doc.mjs`);
|
|
15
|
+
const targetDoc = fs.existsSync(candidateDoc) ? candidateDoc : fs.existsSync(directDoc) ? directDoc : null;
|
|
16
|
+
if (targetDoc) {
|
|
17
|
+
found = true;
|
|
18
|
+
const content = fs.readFileSync(targetDoc, 'utf8');
|
|
19
|
+
// Extract description
|
|
20
|
+
const descMatch = content.match(/description:\s*['"`](.*?)['"`]/);
|
|
21
|
+
const description = descMatch ? descMatch[1] : '';
|
|
22
|
+
// Extract props roughly from JSDoc or object declaration
|
|
23
|
+
const propMatches = [...content.matchAll(/name:\s*['"`](.*?)['"`],\s*type:\s*['"`](.*?)['"`]/g)];
|
|
24
|
+
const props = propMatches.map((m) => ({
|
|
25
|
+
name: m[1],
|
|
26
|
+
type: m[2],
|
|
27
|
+
}));
|
|
28
|
+
// Default design tokens
|
|
29
|
+
const tokens = [];
|
|
30
|
+
const tokenMatches = [...content.matchAll(/--[a-z0-9-]+/g)];
|
|
31
|
+
for (const tm of tokenMatches) {
|
|
32
|
+
if (!tokens.includes(tm[0]))
|
|
33
|
+
tokens.push(tm[0]);
|
|
34
|
+
}
|
|
35
|
+
result[name] = {
|
|
36
|
+
name,
|
|
37
|
+
found: true,
|
|
38
|
+
package: pkg,
|
|
39
|
+
props: props.length > 0 ? props : [
|
|
40
|
+
{ name: 'size', type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'" },
|
|
41
|
+
{ name: 'variant', type: "'solid' | 'outline' | 'ghost' | 'soft'" },
|
|
42
|
+
{ name: 'tone', type: "'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'info'" },
|
|
43
|
+
{ name: 'motion', type: "'none' | 'subtle' | 'spring'" },
|
|
44
|
+
],
|
|
45
|
+
tokens: tokens.length > 0 ? tokens : ['--color-primary-base', '--spacing-2', '--radius-md'],
|
|
46
|
+
variants: ['solid', 'outline', 'ghost', 'soft'],
|
|
47
|
+
tones: ['primary', 'neutral', 'success', 'warning', 'danger', 'info'],
|
|
48
|
+
sizes: ['xs', 'sm', 'md', 'lg', 'xl'],
|
|
49
|
+
denseUsage: `import { ${name} } from '${pkg}';\n<${name} variant="solid" tone="primary" size="md">Content</${name}>`,
|
|
50
|
+
};
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!found) {
|
|
55
|
+
result[name] = {
|
|
56
|
+
name,
|
|
57
|
+
found: false,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function dsSearch(query, category, rootDir = process.cwd()) {
|
|
4
|
+
const results = [];
|
|
5
|
+
const normalizedQuery = query.toLowerCase().trim();
|
|
6
|
+
const packagesToScan = [
|
|
7
|
+
{ pkgName: '@blue-patterns/core', relDir: 'packages/core/src', defaultCategory: 'Component' },
|
|
8
|
+
{ pkgName: '@blue-patterns/charts', relDir: 'packages/charts/src', defaultCategory: 'DataViz' },
|
|
9
|
+
{ pkgName: '@blue-patterns/motion', relDir: 'packages/motion/src', defaultCategory: 'Motion' },
|
|
10
|
+
];
|
|
11
|
+
for (const { pkgName, relDir, defaultCategory } of packagesToScan) {
|
|
12
|
+
const fullDir = path.resolve(rootDir, relDir);
|
|
13
|
+
if (!fs.existsSync(fullDir))
|
|
14
|
+
continue;
|
|
15
|
+
const entries = fs.readdirSync(fullDir, { withFileTypes: true });
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (entry.isDirectory()) {
|
|
18
|
+
const docFile = path.join(fullDir, entry.name, `${entry.name}.doc.mjs`);
|
|
19
|
+
if (fs.existsSync(docFile)) {
|
|
20
|
+
const content = fs.readFileSync(docFile, 'utf8');
|
|
21
|
+
const nameMatch = entry.name;
|
|
22
|
+
const descMatch = content.match(/description:\s*['"`](.*?)['"`]/);
|
|
23
|
+
const description = descMatch ? descMatch[1] : `Component ${nameMatch}`;
|
|
24
|
+
let score = 0;
|
|
25
|
+
if (nameMatch.toLowerCase() === normalizedQuery)
|
|
26
|
+
score += 100;
|
|
27
|
+
else if (nameMatch.toLowerCase().includes(normalizedQuery))
|
|
28
|
+
score += 50;
|
|
29
|
+
if (description.toLowerCase().includes(normalizedQuery))
|
|
30
|
+
score += 20;
|
|
31
|
+
if (score > 0 || !query) {
|
|
32
|
+
results.push({
|
|
33
|
+
name: nameMatch,
|
|
34
|
+
package: pkgName,
|
|
35
|
+
category: defaultCategory,
|
|
36
|
+
description,
|
|
37
|
+
score,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Also include 8 Themes
|
|
45
|
+
const themes = [
|
|
46
|
+
{ name: 'theme-default.css', desc: 'Balanced neutral baseline' },
|
|
47
|
+
{ name: 'theme-obsidian.css', desc: 'High-contrast developer dark-first palette' },
|
|
48
|
+
{ name: 'theme-editorial.css', desc: 'Warm paper surfaces + serif display typography' },
|
|
49
|
+
{ name: 'theme-enterprise.css', desc: 'High-density spacing and compact scale for data-heavy SaaS' },
|
|
50
|
+
{ name: 'theme-glass.css', desc: 'Translucent elevated surfaces with backdrop-filter tokens' },
|
|
51
|
+
{ name: 'theme-forest.css', desc: 'Organic deep-green & warm sand tokens' },
|
|
52
|
+
{ name: 'theme-cobalt.css', desc: 'Crisp fintech blue & slate hierarchy' },
|
|
53
|
+
{ name: 'theme-high-contrast.css', desc: 'WCAG AAA 7:1+ contrast enforcement' },
|
|
54
|
+
];
|
|
55
|
+
for (const th of themes) {
|
|
56
|
+
let score = 0;
|
|
57
|
+
if (th.name.toLowerCase().includes(normalizedQuery))
|
|
58
|
+
score += 60;
|
|
59
|
+
if (th.desc.toLowerCase().includes(normalizedQuery))
|
|
60
|
+
score += 25;
|
|
61
|
+
if (score > 0 || !query) {
|
|
62
|
+
results.push({
|
|
63
|
+
name: th.name,
|
|
64
|
+
package: '@blue-patterns/themes',
|
|
65
|
+
category: 'Theme',
|
|
66
|
+
description: th.desc,
|
|
67
|
+
score,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return results
|
|
72
|
+
.filter((r) => !category || r.category.toLowerCase() === category.toLowerCase())
|
|
73
|
+
.sort((a, b) => b.score - a.score);
|
|
74
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function dsSwizzle(componentName, destinationPath, rootDir = process.cwd()) {
|
|
4
|
+
const componentDir = path.resolve(rootDir, 'packages/core/src', componentName);
|
|
5
|
+
if (!fs.existsSync(componentDir)) {
|
|
6
|
+
return {
|
|
7
|
+
success: false,
|
|
8
|
+
component: componentName,
|
|
9
|
+
destination: destinationPath,
|
|
10
|
+
filesCopied: [],
|
|
11
|
+
error: `Component directory not found at '${componentDir}'.`,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const destDir = path.resolve(rootDir, destinationPath, componentName);
|
|
15
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
16
|
+
const filesCopied = [];
|
|
17
|
+
const entries = fs.readdirSync(componentDir, { withFileTypes: true });
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (entry.isFile()) {
|
|
20
|
+
const srcFile = path.join(componentDir, entry.name);
|
|
21
|
+
const dstFile = path.join(destDir, entry.name);
|
|
22
|
+
fs.copyFileSync(srcFile, dstFile);
|
|
23
|
+
filesCopied.push(entry.name);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
success: true,
|
|
28
|
+
component: componentName,
|
|
29
|
+
destination: destDir,
|
|
30
|
+
filesCopied,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ValidationIssue {
|
|
2
|
+
type: 'hallucinated_component' | 'hardcoded_color' | 'invalid_prop' | 'untokenized_style';
|
|
3
|
+
message: string;
|
|
4
|
+
line?: number;
|
|
5
|
+
column?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ValidationResult {
|
|
8
|
+
valid: boolean;
|
|
9
|
+
issues: ValidationIssue[];
|
|
10
|
+
summary: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function dsValidateJsx(tsxCode: string): ValidationResult;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
export function dsValidateJsx(tsxCode) {
|
|
3
|
+
const issues = [];
|
|
4
|
+
const sourceFile = ts.createSourceFile('input.tsx', tsxCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
5
|
+
// 1. Check for raw hex colors in code text
|
|
6
|
+
const hexColorRegex = /#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g;
|
|
7
|
+
let match;
|
|
8
|
+
while ((match = hexColorRegex.exec(tsxCode)) !== null) {
|
|
9
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(match.index);
|
|
10
|
+
issues.push({
|
|
11
|
+
type: 'hardcoded_color',
|
|
12
|
+
message: `Hardcoded hex color '${match[0]}' detected. Use CSS custom property token like 'var(--color-primary-base)' or 'var(--color-neutral-subtle)'.`,
|
|
13
|
+
line: pos.line + 1,
|
|
14
|
+
column: pos.character + 1,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
// 2. Check imports & JSX Elements AST
|
|
18
|
+
function visit(node) {
|
|
19
|
+
// Check imports from @blue-patterns/*
|
|
20
|
+
if (ts.isImportDeclaration(node)) {
|
|
21
|
+
const moduleSpecifier = node.moduleSpecifier.getText(sourceFile).replace(/['"]/g, '');
|
|
22
|
+
if (moduleSpecifier.startsWith('@astryxdesign/')) {
|
|
23
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
24
|
+
issues.push({
|
|
25
|
+
type: 'hallucinated_component',
|
|
26
|
+
message: `Legacy import '${moduleSpecifier}'. Use re-namespaced '@blue-patterns/${moduleSpecifier.replace('@astryxdesign/', '')}'.`,
|
|
27
|
+
line: pos.line + 1,
|
|
28
|
+
column: pos.character + 1,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Check JSX attributes for invalid styling patterns
|
|
33
|
+
if (ts.isJsxAttribute(node)) {
|
|
34
|
+
const attrName = node.name.getText(sourceFile);
|
|
35
|
+
if (attrName === 'style' && node.initializer) {
|
|
36
|
+
const text = node.initializer.getText(sourceFile);
|
|
37
|
+
if (/px\b/.test(text) && !/var\(--spacing-/.test(text)) {
|
|
38
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
39
|
+
issues.push({
|
|
40
|
+
type: 'untokenized_style',
|
|
41
|
+
message: `Inline hardcoded pixel value detected in style attribute. Use design tokens like 'var(--spacing-*)'.`,
|
|
42
|
+
line: pos.line + 1,
|
|
43
|
+
column: pos.character + 1,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
ts.forEachChild(node, visit);
|
|
49
|
+
}
|
|
50
|
+
visit(sourceFile);
|
|
51
|
+
return {
|
|
52
|
+
valid: issues.length === 0,
|
|
53
|
+
issues,
|
|
54
|
+
summary: issues.length === 0
|
|
55
|
+
? 'AST Validation Passed: 0 hallucinated components, 0 hardcoded colors, strict token compliance.'
|
|
56
|
+
: `AST Validation Failed: ${issues.length} issue(s) detected.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blue-patterns/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.7.18",
|
|
4
|
+
"description": "Deterministic Model Context Protocol (MCP) server exposing Blue-Patterns-UI contracts, validation, and search to AI coding agents",
|
|
5
|
+
"author": "Blue-Patterns-UI Contributors",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"bin",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"bin": {
|
|
19
|
+
"blue-patterns-mcp": "./bin/mcp-server.js"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"typescript": "^5.0.0 || ^6.0.0"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/Sandeep-Hipparagi/blue-patterns-ui#readme",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/Sandeep-Hipparagi/blue-patterns-ui.git",
|
|
34
|
+
"directory": "packages/mcp"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/Sandeep-Hipparagi/blue-patterns-ui/issues"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.json",
|
|
41
|
+
"start": "node bin/mcp-server.js",
|
|
42
|
+
"typecheck": "tsc --noEmit"
|
|
43
|
+
}
|
|
6
44
|
}
|