mcp-probe-kit 3.0.19 → 3.0.22
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 +12 -5
- package/build/index.js +3 -1
- package/build/lib/__tests__/agents-md-template.unit.test.js +2 -0
- package/build/lib/__tests__/memory-config.unit.test.js +9 -0
- package/build/lib/__tests__/memory-injection.unit.test.d.ts +1 -0
- package/build/lib/__tests__/memory-injection.unit.test.js +51 -0
- package/build/lib/__tests__/memory-orchestration.unit.test.d.ts +1 -0
- package/build/lib/__tests__/memory-orchestration.unit.test.js +84 -0
- package/build/lib/__tests__/memory-payload.unit.test.d.ts +1 -0
- package/build/lib/__tests__/memory-payload.unit.test.js +35 -0
- package/build/lib/agents-md-template.js +7 -5
- package/build/lib/memory-client.d.ts +8 -1
- package/build/lib/memory-client.js +53 -44
- package/build/lib/memory-config.d.ts +8 -0
- package/build/lib/memory-config.js +19 -0
- package/build/lib/memory-orchestration.d.ts +7 -2
- package/build/lib/memory-orchestration.js +81 -8
- package/build/lib/memory-payload.d.ts +21 -0
- package/build/lib/memory-payload.js +65 -0
- package/build/lib/shadcn-ui.d.ts +11 -0
- package/build/lib/shadcn-ui.js +78 -0
- package/build/resources/ui-ux-data/guidelines/vercel-web-interface.json +1632 -0
- package/build/resources/ui-ux-data/metadata.json +27 -3
- package/build/resources/ui-ux-data/shadcn/blocks.json +2541 -0
- package/build/resources/ui-ux-data/shadcn/components.json +997 -0
- package/build/resources/ui-ux-data/themes/presets.json +483 -0
- package/build/schemas/index.d.ts +38 -9
- package/build/schemas/memory-tools.d.ts +38 -9
- package/build/schemas/memory-tools.js +24 -9
- package/build/schemas/output/ui-ux-tools.d.ts +16 -0
- package/build/schemas/output/ui-ux-tools.js +4 -0
- package/build/schemas/ui-ux-schemas.js +3 -3
- package/build/tools/__tests__/start_ui.property.test.js +4 -3
- package/build/tools/index.d.ts +1 -0
- package/build/tools/index.js +1 -0
- package/build/tools/memorize_asset.js +12 -0
- package/build/tools/scan_and_extract_patterns.js +7 -7
- package/build/tools/search_memory.d.ts +7 -0
- package/build/tools/search_memory.js +57 -0
- package/build/tools/start_bugfix.js +3 -3
- package/build/tools/start_feature.js +3 -3
- package/build/tools/start_ui.js +33 -6
- package/build/tools/ui-ux-tools.js +322 -244
- package/build/utils/__tests__/shadcn-sync.unit.test.d.ts +1 -0
- package/build/utils/__tests__/shadcn-sync.unit.test.js +49 -0
- package/build/utils/__tests__/theme-pick.unit.test.d.ts +1 -0
- package/build/utils/__tests__/theme-pick.unit.test.js +9 -0
- package/build/utils/__tests__/themes-sync.unit.test.d.ts +1 -0
- package/build/utils/__tests__/themes-sync.unit.test.js +21 -0
- package/build/utils/__tests__/ui-metadata.unit.test.d.ts +1 -0
- package/build/utils/__tests__/ui-metadata.unit.test.js +35 -0
- package/build/utils/__tests__/vercel-guidelines-sync.unit.test.d.ts +1 -0
- package/build/utils/__tests__/vercel-guidelines-sync.unit.test.js +34 -0
- package/build/utils/bm25.d.ts +2 -1
- package/build/utils/bm25.js +17 -5
- package/build/utils/shadcn-sync.d.ts +55 -0
- package/build/utils/shadcn-sync.js +146 -0
- package/build/utils/themes-sync.d.ts +32 -0
- package/build/utils/themes-sync.js +201 -0
- package/build/utils/ui-data-loader.js +13 -2
- package/build/utils/ui-metadata.d.ts +27 -0
- package/build/utils/ui-metadata.js +39 -0
- package/build/utils/ui-search-engine.d.ts +1 -0
- package/build/utils/ui-search-engine.js +20 -6
- package/build/utils/ui-sync.d.ts +24 -2
- package/build/utils/ui-sync.js +152 -86
- package/build/utils/vercel-guidelines-sync.d.ts +30 -0
- package/build/utils/vercel-guidelines-sync.js +133 -0
- package/docs/data/tools.js +18 -0
- package/docs/i18n/all-tools/en.json +6 -1
- package/docs/i18n/all-tools/ja.json +2 -1
- package/docs/i18n/all-tools/ko.json +2 -1
- package/docs/i18n/all-tools/zh-CN.json +7 -2
- package/docs/i18n/en.json +5 -5
- package/docs/i18n/ja.json +2 -2
- package/docs/i18n/ko.json +2 -2
- package/docs/i18n/zh-CN.json +7 -7
- package/docs/memory-local-setup.md +1 -1
- package/docs/memory-local-setup.zh-CN.md +5 -2
- package/docs/pages/getting-started.html +3 -2
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { computeRegistryChecksum, transformRegistryItems, } from '../shadcn-sync.js';
|
|
3
|
+
const sampleRegistry = {
|
|
4
|
+
name: 'shadcn/ui',
|
|
5
|
+
items: [
|
|
6
|
+
{
|
|
7
|
+
name: 'button',
|
|
8
|
+
type: 'registry:ui',
|
|
9
|
+
description: 'Button component',
|
|
10
|
+
registryDependencies: ['utils'],
|
|
11
|
+
files: [{ path: 'registry/new-york/ui/button.tsx', target: '' }],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'dashboard-01',
|
|
15
|
+
type: 'registry:block',
|
|
16
|
+
description: 'A dashboard with sidebar, charts and data table.',
|
|
17
|
+
registryDependencies: ['sidebar', 'chart', 'button'],
|
|
18
|
+
files: [
|
|
19
|
+
{ path: 'registry/new-york/blocks/dashboard-01/page.tsx', target: 'app/dashboard/page.tsx' },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'index',
|
|
24
|
+
type: 'registry:style',
|
|
25
|
+
files: [],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
describe('shadcn-sync', () => {
|
|
30
|
+
it('transforms registry items into searchable blocks and components', () => {
|
|
31
|
+
const { blocks, components } = transformRegistryItems(sampleRegistry);
|
|
32
|
+
expect(blocks).toHaveLength(1);
|
|
33
|
+
expect(components).toHaveLength(1);
|
|
34
|
+
expect(blocks[0].name).toBe('dashboard-01');
|
|
35
|
+
expect(blocks[0].category).toBe('shadcn-blocks');
|
|
36
|
+
expect(blocks[0].installCommand).toBe('npx shadcn@latest add dashboard-01');
|
|
37
|
+
expect(components[0].name).toBe('button');
|
|
38
|
+
expect(components[0].category).toBe('shadcn-components');
|
|
39
|
+
});
|
|
40
|
+
it('computes stable checksum for registry signature', () => {
|
|
41
|
+
const first = computeRegistryChecksum(sampleRegistry);
|
|
42
|
+
const second = computeRegistryChecksum({
|
|
43
|
+
...sampleRegistry,
|
|
44
|
+
items: [...sampleRegistry.items].reverse(),
|
|
45
|
+
});
|
|
46
|
+
expect(first).toBe(second);
|
|
47
|
+
expect(first).toHaveLength(16);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { pickThemeForProductType } from '../../lib/shadcn-ui.js';
|
|
3
|
+
import { CURATED_UI_THEMES } from '../themes-sync.js';
|
|
4
|
+
describe('pickThemeForProductType', () => {
|
|
5
|
+
it('prefers themes whose bestFor matches product type', () => {
|
|
6
|
+
const picked = pickThemeForProductType(CURATED_UI_THEMES, 'Healthcare App');
|
|
7
|
+
expect(picked?.name).toBe('green-health');
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { CURATED_UI_THEMES, computeThemesChecksum, syncThemesTo } from '../themes-sync.js';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
describe('themes-sync', () => {
|
|
7
|
+
it('ships curated shadcn-compatible theme presets', () => {
|
|
8
|
+
expect(CURATED_UI_THEMES.length).toBeGreaterThanOrEqual(8);
|
|
9
|
+
const blue = CURATED_UI_THEMES.find((theme) => theme.name === 'blue-saas');
|
|
10
|
+
expect(blue?.globalsCssSnippet).toContain('--primary:');
|
|
11
|
+
expect(blue?.category).toBe('ui-themes');
|
|
12
|
+
});
|
|
13
|
+
it('writes presets.json when checksum changes', () => {
|
|
14
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ui-themes-'));
|
|
15
|
+
const result = syncThemesTo(dir, { force: true });
|
|
16
|
+
expect(result?.count).toBe(CURATED_UI_THEMES.length);
|
|
17
|
+
expect(fs.existsSync(path.join(dir, 'themes', 'presets.json'))).toBe(true);
|
|
18
|
+
const again = syncThemesTo(dir, { existingChecksum: computeThemesChecksum() });
|
|
19
|
+
expect(again).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { readUISyncMetadata, writeUISyncMetadata } from '../ui-metadata.js';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
describe('ui-metadata', () => {
|
|
7
|
+
it('migrates legacy metadata into sources map', () => {
|
|
8
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ui-meta-'));
|
|
9
|
+
fs.writeFileSync(path.join(dir, 'metadata.json'), JSON.stringify({
|
|
10
|
+
version: '2.2.3',
|
|
11
|
+
syncedAt: '2026-01-01T00:00:00.000Z',
|
|
12
|
+
source: 'uipro-cli',
|
|
13
|
+
format: 'json',
|
|
14
|
+
}));
|
|
15
|
+
const metadata = readUISyncMetadata(dir);
|
|
16
|
+
expect(metadata?.sources['uipro-cli']?.version).toBe('2.2.3');
|
|
17
|
+
expect(metadata?.version).toBe('2.2.3');
|
|
18
|
+
});
|
|
19
|
+
it('writes metadata with trailing newline', () => {
|
|
20
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ui-meta-'));
|
|
21
|
+
writeUISyncMetadata(dir, {
|
|
22
|
+
version: '2.2.3',
|
|
23
|
+
syncedAt: '2026-01-01T00:00:00.000Z',
|
|
24
|
+
source: 'uipro-cli',
|
|
25
|
+
format: 'json',
|
|
26
|
+
sources: {
|
|
27
|
+
'uipro-cli': { version: '2.2.3', syncedAt: '2026-01-01T00:00:00.000Z' },
|
|
28
|
+
shadcn: { version: 'abc123', syncedAt: '2026-01-01T00:00:00.000Z', blocks: 10, components: 20 },
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
const raw = fs.readFileSync(path.join(dir, 'metadata.json'), 'utf-8');
|
|
32
|
+
expect(raw.endsWith('\n')).toBe(true);
|
|
33
|
+
expect(JSON.parse(raw).sources.shadcn.blocks).toBe(10);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseVercelGuidelinesMarkdown } from '../vercel-guidelines-sync.js';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
describe('vercel-guidelines-sync', () => {
|
|
6
|
+
it('parses MUST/SHOULD/NEVER rules from markdown', () => {
|
|
7
|
+
const sample = `# Title
|
|
8
|
+
|
|
9
|
+
## Interactions
|
|
10
|
+
|
|
11
|
+
### Keyboard
|
|
12
|
+
|
|
13
|
+
- MUST: Visible focus rings
|
|
14
|
+
- NEVER: outline: none without replacement
|
|
15
|
+
|
|
16
|
+
## Animation
|
|
17
|
+
|
|
18
|
+
- SHOULD: Prefer CSS animations
|
|
19
|
+
`;
|
|
20
|
+
const records = parseVercelGuidelinesMarkdown(sample);
|
|
21
|
+
expect(records).toHaveLength(3);
|
|
22
|
+
expect(records[0].level).toBe('MUST');
|
|
23
|
+
expect(records[0].section).toBe('Interactions');
|
|
24
|
+
expect(records[0].category).toBe('ui-guidelines-vercel');
|
|
25
|
+
expect(records[1].level).toBe('NEVER');
|
|
26
|
+
});
|
|
27
|
+
it('parses bundled fallback markdown file', () => {
|
|
28
|
+
const fallback = path.join(process.cwd(), 'scripts', 'data', 'vercel-web-interface-guidelines.md');
|
|
29
|
+
const markdown = fs.readFileSync(fallback, 'utf-8');
|
|
30
|
+
const records = parseVercelGuidelinesMarkdown(markdown);
|
|
31
|
+
expect(records.length).toBeGreaterThan(50);
|
|
32
|
+
expect(records.some((item) => item.rule.includes('prefers-reduced-motion'))).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
package/build/utils/bm25.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export declare class BM25 {
|
|
|
41
41
|
* 批量添加文档
|
|
42
42
|
*/
|
|
43
43
|
addDocuments(docs: BM25Document[]): void;
|
|
44
|
+
private recalculateAvgDocLength;
|
|
44
45
|
/**
|
|
45
46
|
* 计算 IDF (Inverse Document Frequency)
|
|
46
47
|
*/
|
|
@@ -52,7 +53,7 @@ export declare class BM25 {
|
|
|
52
53
|
/**
|
|
53
54
|
* 搜索文档
|
|
54
55
|
*/
|
|
55
|
-
search(query: string, limit?: number): BM25SearchResult[];
|
|
56
|
+
search(query: string, limit?: number, predicate?: (metadata?: Record<string, any>) => boolean): BM25SearchResult[];
|
|
56
57
|
/**
|
|
57
58
|
* 清空索引
|
|
58
59
|
*/
|
package/build/utils/bm25.js
CHANGED
|
@@ -46,6 +46,7 @@ export class BM25 {
|
|
|
46
46
|
}
|
|
47
47
|
this.invertedIndex.get(term).set(doc.id, freq);
|
|
48
48
|
}
|
|
49
|
+
this.recalculateAvgDocLength();
|
|
49
50
|
}
|
|
50
51
|
/**
|
|
51
52
|
* 批量添加文档
|
|
@@ -54,7 +55,12 @@ export class BM25 {
|
|
|
54
55
|
for (const doc of docs) {
|
|
55
56
|
this.addDocument(doc);
|
|
56
57
|
}
|
|
57
|
-
|
|
58
|
+
}
|
|
59
|
+
recalculateAvgDocLength() {
|
|
60
|
+
if (this.documents.length === 0) {
|
|
61
|
+
this.avgDocLength = 0;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
58
64
|
let totalLength = 0;
|
|
59
65
|
for (const length of this.documentLengths.values()) {
|
|
60
66
|
totalLength += length;
|
|
@@ -94,7 +100,7 @@ export class BM25 {
|
|
|
94
100
|
/**
|
|
95
101
|
* 搜索文档
|
|
96
102
|
*/
|
|
97
|
-
search(query, limit = 10) {
|
|
103
|
+
search(query, limit = 10, predicate) {
|
|
98
104
|
const queryTerms = this.tokenize(query);
|
|
99
105
|
if (queryTerms.length === 0) {
|
|
100
106
|
return [];
|
|
@@ -112,9 +118,15 @@ export class BM25 {
|
|
|
112
118
|
// 计算得分
|
|
113
119
|
const results = [];
|
|
114
120
|
for (const docId of relevantDocs) {
|
|
115
|
-
const score = this.calculateScore(docId, queryTerms);
|
|
116
121
|
const doc = this.documents.find(d => d.id === docId);
|
|
117
|
-
if (doc
|
|
122
|
+
if (!doc) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (predicate && !predicate(doc.metadata)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const score = this.calculateScore(docId, queryTerms);
|
|
129
|
+
if (score > 0) {
|
|
118
130
|
results.push({
|
|
119
131
|
id: docId,
|
|
120
132
|
score,
|
|
@@ -134,6 +146,6 @@ export class BM25 {
|
|
|
134
146
|
this.documents = [];
|
|
135
147
|
this.invertedIndex.clear();
|
|
136
148
|
this.documentLengths.clear();
|
|
137
|
-
this.
|
|
149
|
+
this.recalculateAvgDocLength();
|
|
138
150
|
}
|
|
139
151
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shadcn/ui registry 同步(blocks + components 索引)
|
|
3
|
+
*/
|
|
4
|
+
import type { SyncRuntimeOptions } from './ui-sync.js';
|
|
5
|
+
import type { UISourceMetadata } from './ui-metadata.js';
|
|
6
|
+
export declare const SHADCN_REGISTRY_URL = "https://ui.shadcn.com/r/styles/new-york/registry.json";
|
|
7
|
+
export declare const SHADCN_STYLE = "new-york";
|
|
8
|
+
export interface ShadcnRegistryItem {
|
|
9
|
+
name: string;
|
|
10
|
+
type?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
dependencies?: string[];
|
|
13
|
+
registryDependencies?: string[];
|
|
14
|
+
files?: Array<{
|
|
15
|
+
path?: string;
|
|
16
|
+
target?: string;
|
|
17
|
+
type?: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
export interface ShadcnRegistry {
|
|
21
|
+
name?: string;
|
|
22
|
+
homepage?: string;
|
|
23
|
+
items: ShadcnRegistryItem[];
|
|
24
|
+
}
|
|
25
|
+
export interface ShadcnSearchRecord {
|
|
26
|
+
name: string;
|
|
27
|
+
title: string;
|
|
28
|
+
description: string;
|
|
29
|
+
type: 'block' | 'component' | 'other';
|
|
30
|
+
stack: string;
|
|
31
|
+
style: string;
|
|
32
|
+
dependencies: string[];
|
|
33
|
+
registryDependencies: string[];
|
|
34
|
+
fileCount: number;
|
|
35
|
+
files: string[];
|
|
36
|
+
installCommand: string;
|
|
37
|
+
category: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ShadcnSyncResult {
|
|
40
|
+
metadata: UISourceMetadata;
|
|
41
|
+
blocks: number;
|
|
42
|
+
components: number;
|
|
43
|
+
}
|
|
44
|
+
export declare function fetchText(url: string, signal?: AbortSignal): Promise<string>;
|
|
45
|
+
export declare function fetchJson<T>(url: string, signal?: AbortSignal): Promise<T>;
|
|
46
|
+
export declare function computeRegistryChecksum(registry: ShadcnRegistry): string;
|
|
47
|
+
export declare function transformRegistryItems(registry: ShadcnRegistry): {
|
|
48
|
+
blocks: ShadcnSearchRecord[];
|
|
49
|
+
components: ShadcnSearchRecord[];
|
|
50
|
+
};
|
|
51
|
+
export declare function fetchShadcnRegistry(signal?: AbortSignal): Promise<ShadcnRegistry>;
|
|
52
|
+
export declare function syncShadcnTo(outputDir: string, options?: SyncRuntimeOptions & {
|
|
53
|
+
force?: boolean;
|
|
54
|
+
existingChecksum?: string;
|
|
55
|
+
}): Promise<ShadcnSyncResult | null>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shadcn/ui registry 同步(blocks + components 索引)
|
|
3
|
+
*/
|
|
4
|
+
import * as crypto from 'crypto';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as https from 'https';
|
|
8
|
+
export const SHADCN_REGISTRY_URL = 'https://ui.shadcn.com/r/styles/new-york/registry.json';
|
|
9
|
+
export const SHADCN_STYLE = 'new-york';
|
|
10
|
+
function throwIfAborted(signal, message) {
|
|
11
|
+
if (!signal?.aborted)
|
|
12
|
+
return;
|
|
13
|
+
const err = new Error(message);
|
|
14
|
+
err.name = 'AbortError';
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
export function fetchText(url, signal) {
|
|
18
|
+
throwIfAborted(signal, 'Sync cancelled before fetch');
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const req = https.get(url, (res) => {
|
|
21
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
22
|
+
const location = res.headers.location;
|
|
23
|
+
if (location) {
|
|
24
|
+
fetchText(location, signal).then(resolve).catch(reject);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (res.statusCode !== 200) {
|
|
29
|
+
reject(new Error(`Failed to fetch ${url}: HTTP ${res.statusCode}`));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
let data = '';
|
|
33
|
+
res.on('data', (chunk) => {
|
|
34
|
+
if (signal?.aborted) {
|
|
35
|
+
req.destroy(new Error('Sync cancelled while reading response'));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
data += chunk;
|
|
39
|
+
});
|
|
40
|
+
res.on('end', () => {
|
|
41
|
+
throwIfAborted(signal, 'Sync cancelled after fetch');
|
|
42
|
+
resolve(data);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
const onAbort = () => req.destroy(new Error('Sync cancelled'));
|
|
46
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
47
|
+
req.on('error', (error) => {
|
|
48
|
+
signal?.removeEventListener('abort', onAbort);
|
|
49
|
+
reject(error);
|
|
50
|
+
});
|
|
51
|
+
req.on('close', () => {
|
|
52
|
+
signal?.removeEventListener('abort', onAbort);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export function fetchJson(url, signal) {
|
|
57
|
+
return fetchText(url, signal).then((data) => {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(data);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw new Error(`Failed to parse JSON from ${url}: ${error}`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export function computeRegistryChecksum(registry) {
|
|
67
|
+
const signature = (registry.items || [])
|
|
68
|
+
.map((item) => `${item.name}:${item.type || ''}:${item.description || ''}`)
|
|
69
|
+
.sort()
|
|
70
|
+
.join('|');
|
|
71
|
+
return crypto.createHash('sha256').update(signature).digest('hex').slice(0, 16);
|
|
72
|
+
}
|
|
73
|
+
function normalizeType(raw) {
|
|
74
|
+
if (raw === 'registry:block')
|
|
75
|
+
return 'block';
|
|
76
|
+
if (raw === 'registry:ui')
|
|
77
|
+
return 'component';
|
|
78
|
+
return 'other';
|
|
79
|
+
}
|
|
80
|
+
function buildInstallCommand(name, type) {
|
|
81
|
+
if (type === 'block' || type === 'component') {
|
|
82
|
+
return `npx shadcn@latest add ${name}`;
|
|
83
|
+
}
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
export function transformRegistryItems(registry) {
|
|
87
|
+
const blocks = [];
|
|
88
|
+
const components = [];
|
|
89
|
+
for (const item of registry.items || []) {
|
|
90
|
+
const type = normalizeType(item.type);
|
|
91
|
+
if (type === 'other')
|
|
92
|
+
continue;
|
|
93
|
+
const files = (item.files || [])
|
|
94
|
+
.map((file) => file.target || file.path || '')
|
|
95
|
+
.filter(Boolean);
|
|
96
|
+
const record = {
|
|
97
|
+
name: item.name,
|
|
98
|
+
title: item.name,
|
|
99
|
+
description: item.description || `${type} from shadcn/ui (${SHADCN_STYLE})`,
|
|
100
|
+
type,
|
|
101
|
+
stack: 'react',
|
|
102
|
+
style: SHADCN_STYLE,
|
|
103
|
+
dependencies: item.dependencies || [],
|
|
104
|
+
registryDependencies: item.registryDependencies || [],
|
|
105
|
+
fileCount: files.length,
|
|
106
|
+
files,
|
|
107
|
+
installCommand: buildInstallCommand(item.name, type),
|
|
108
|
+
category: type === 'block' ? 'shadcn-blocks' : 'shadcn-components',
|
|
109
|
+
};
|
|
110
|
+
if (type === 'block') {
|
|
111
|
+
blocks.push(record);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
components.push(record);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { blocks, components };
|
|
118
|
+
}
|
|
119
|
+
export async function fetchShadcnRegistry(signal) {
|
|
120
|
+
return fetchJson(SHADCN_REGISTRY_URL, signal);
|
|
121
|
+
}
|
|
122
|
+
export async function syncShadcnTo(outputDir, options) {
|
|
123
|
+
throwIfAborted(options?.signal, 'shadcn sync cancelled');
|
|
124
|
+
const registry = await fetchShadcnRegistry(options?.signal);
|
|
125
|
+
const checksum = computeRegistryChecksum(registry);
|
|
126
|
+
if (!options?.force && options?.existingChecksum === checksum) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const { blocks, components } = transformRegistryItems(registry);
|
|
130
|
+
const shadcnDir = path.join(outputDir, 'shadcn');
|
|
131
|
+
if (!fs.existsSync(shadcnDir)) {
|
|
132
|
+
fs.mkdirSync(shadcnDir, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
fs.writeFileSync(path.join(shadcnDir, 'blocks.json'), `${JSON.stringify(blocks, null, 2)}\n`, 'utf-8');
|
|
135
|
+
fs.writeFileSync(path.join(shadcnDir, 'components.json'), `${JSON.stringify(components, null, 2)}\n`, 'utf-8');
|
|
136
|
+
const syncedAt = new Date().toISOString();
|
|
137
|
+
const metadata = {
|
|
138
|
+
version: checksum,
|
|
139
|
+
syncedAt,
|
|
140
|
+
checksum,
|
|
141
|
+
style: SHADCN_STYLE,
|
|
142
|
+
blocks: blocks.length,
|
|
143
|
+
components: components.length,
|
|
144
|
+
};
|
|
145
|
+
return { metadata, blocks: blocks.length, components: components.length };
|
|
146
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shadcn/ui 兼容主题预设(CSS variables,非 tweakcn 全量 fork)
|
|
3
|
+
* 灵感来源:shadcn themes + tweakcn 风格的现代变量结构
|
|
4
|
+
*/
|
|
5
|
+
import type { UISourceMetadata } from './ui-metadata.js';
|
|
6
|
+
export declare const UI_THEMES_VERSION = "1.0.0";
|
|
7
|
+
export type ThemeCssVars = Record<string, string>;
|
|
8
|
+
export interface UIThemeRecord {
|
|
9
|
+
name: string;
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
style: 'new-york';
|
|
13
|
+
stack: string;
|
|
14
|
+
baseColor: string;
|
|
15
|
+
mood: string;
|
|
16
|
+
bestFor: string[];
|
|
17
|
+
cssVarsLight: ThemeCssVars;
|
|
18
|
+
cssVarsDark: ThemeCssVars;
|
|
19
|
+
globalsCssSnippet: string;
|
|
20
|
+
category: string;
|
|
21
|
+
source: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ThemesSyncResult {
|
|
24
|
+
metadata: UISourceMetadata;
|
|
25
|
+
count: number;
|
|
26
|
+
}
|
|
27
|
+
export declare const CURATED_UI_THEMES: UIThemeRecord[];
|
|
28
|
+
export declare function computeThemesChecksum(themes?: UIThemeRecord[]): string;
|
|
29
|
+
export declare function syncThemesTo(outputDir: string, options?: {
|
|
30
|
+
force?: boolean;
|
|
31
|
+
existingChecksum?: string;
|
|
32
|
+
}): ThemesSyncResult | null;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shadcn/ui 兼容主题预设(CSS variables,非 tweakcn 全量 fork)
|
|
3
|
+
* 灵感来源:shadcn themes + tweakcn 风格的现代变量结构
|
|
4
|
+
*/
|
|
5
|
+
import * as crypto from 'crypto';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
export const UI_THEMES_VERSION = '1.0.0';
|
|
9
|
+
const BASE_LIGHT = {
|
|
10
|
+
background: '0 0% 100%',
|
|
11
|
+
foreground: '240 10% 3.9%',
|
|
12
|
+
card: '0 0% 100%',
|
|
13
|
+
'card-foreground': '240 10% 3.9%',
|
|
14
|
+
popover: '0 0% 100%',
|
|
15
|
+
'popover-foreground': '240 10% 3.9%',
|
|
16
|
+
secondary: '240 4.8% 95.9%',
|
|
17
|
+
'secondary-foreground': '240 5.9% 10%',
|
|
18
|
+
muted: '240 4.8% 95.9%',
|
|
19
|
+
'muted-foreground': '240 3.8% 46.1%',
|
|
20
|
+
accent: '240 4.8% 95.9%',
|
|
21
|
+
'accent-foreground': '240 5.9% 10%',
|
|
22
|
+
destructive: '0 84.2% 60.2%',
|
|
23
|
+
'destructive-foreground': '0 0% 98%',
|
|
24
|
+
border: '240 5.9% 90%',
|
|
25
|
+
input: '240 5.9% 90%',
|
|
26
|
+
ring: '240 5.9% 10%',
|
|
27
|
+
radius: '0.5rem',
|
|
28
|
+
};
|
|
29
|
+
const BASE_DARK = {
|
|
30
|
+
background: '240 10% 3.9%',
|
|
31
|
+
foreground: '0 0% 98%',
|
|
32
|
+
card: '240 10% 3.9%',
|
|
33
|
+
'card-foreground': '0 0% 98%',
|
|
34
|
+
popover: '240 10% 3.9%',
|
|
35
|
+
'popover-foreground': '0 0% 98%',
|
|
36
|
+
secondary: '240 3.7% 15.9%',
|
|
37
|
+
'secondary-foreground': '0 0% 98%',
|
|
38
|
+
muted: '240 3.7% 15.9%',
|
|
39
|
+
'muted-foreground': '240 5% 64.9%',
|
|
40
|
+
accent: '240 3.7% 15.9%',
|
|
41
|
+
'accent-foreground': '0 0% 98%',
|
|
42
|
+
destructive: '0 62.8% 30.6%',
|
|
43
|
+
'destructive-foreground': '0 0% 98%',
|
|
44
|
+
border: '240 3.7% 15.9%',
|
|
45
|
+
input: '240 3.7% 15.9%',
|
|
46
|
+
ring: '240 4.9% 83.9%',
|
|
47
|
+
radius: '0.5rem',
|
|
48
|
+
};
|
|
49
|
+
function withPrimary(light, dark, primaryLight, primaryDark) {
|
|
50
|
+
return {
|
|
51
|
+
light: {
|
|
52
|
+
...light,
|
|
53
|
+
primary: primaryLight,
|
|
54
|
+
'primary-foreground': '0 0% 98%',
|
|
55
|
+
ring: primaryLight,
|
|
56
|
+
},
|
|
57
|
+
dark: {
|
|
58
|
+
...dark,
|
|
59
|
+
primary: primaryDark,
|
|
60
|
+
'primary-foreground': '240 5.9% 10%',
|
|
61
|
+
ring: primaryDark,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function renderGlobalsCss(name, light, dark) {
|
|
66
|
+
const toBlock = (vars) => Object.entries(vars)
|
|
67
|
+
.map(([key, value]) => ` --${key}: ${value};`)
|
|
68
|
+
.join('\n');
|
|
69
|
+
return `@layer base {
|
|
70
|
+
/* Theme: ${name} — paste into globals.css */
|
|
71
|
+
:root {
|
|
72
|
+
${toBlock(light)}
|
|
73
|
+
}
|
|
74
|
+
.dark {
|
|
75
|
+
${toBlock(dark)}
|
|
76
|
+
}
|
|
77
|
+
}`;
|
|
78
|
+
}
|
|
79
|
+
function buildTheme(input) {
|
|
80
|
+
const { light, dark } = withPrimary(BASE_LIGHT, BASE_DARK, input.primaryLight, input.primaryDark);
|
|
81
|
+
return {
|
|
82
|
+
name: input.name,
|
|
83
|
+
title: input.title,
|
|
84
|
+
description: input.description,
|
|
85
|
+
style: 'new-york',
|
|
86
|
+
stack: 'react',
|
|
87
|
+
baseColor: input.baseColor,
|
|
88
|
+
mood: input.mood,
|
|
89
|
+
bestFor: input.bestFor,
|
|
90
|
+
cssVarsLight: light,
|
|
91
|
+
cssVarsDark: dark,
|
|
92
|
+
globalsCssSnippet: renderGlobalsCss(input.name, light, dark),
|
|
93
|
+
category: 'ui-themes',
|
|
94
|
+
source: 'mcp-probe-kit-curated-shadcn',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export const CURATED_UI_THEMES = [
|
|
98
|
+
buildTheme({
|
|
99
|
+
name: 'zinc-neutral',
|
|
100
|
+
title: 'Zinc Neutral',
|
|
101
|
+
description: '默认中性 SaaS 风格,低饱和、专业克制',
|
|
102
|
+
baseColor: 'zinc',
|
|
103
|
+
mood: 'neutral professional',
|
|
104
|
+
bestFor: ['SaaS', 'B2B', 'Admin'],
|
|
105
|
+
primaryLight: '240 5.9% 10%',
|
|
106
|
+
primaryDark: '0 0% 98%',
|
|
107
|
+
}),
|
|
108
|
+
buildTheme({
|
|
109
|
+
name: 'slate-dashboard',
|
|
110
|
+
title: 'Slate Dashboard',
|
|
111
|
+
description: '偏冷灰的数据看板与分析产品',
|
|
112
|
+
baseColor: 'slate',
|
|
113
|
+
mood: 'cool analytical',
|
|
114
|
+
bestFor: ['Analytics Dashboard', 'Fintech'],
|
|
115
|
+
primaryLight: '215.4 16.3% 36.9%',
|
|
116
|
+
primaryDark: '210 40% 98%',
|
|
117
|
+
}),
|
|
118
|
+
buildTheme({
|
|
119
|
+
name: 'blue-saas',
|
|
120
|
+
title: 'Blue SaaS',
|
|
121
|
+
description: '常见 B2B 蓝色主色,清晰可信',
|
|
122
|
+
baseColor: 'blue',
|
|
123
|
+
mood: 'trustworthy modern',
|
|
124
|
+
bestFor: ['SaaS', 'B2B Service'],
|
|
125
|
+
primaryLight: '221.2 83.2% 53.3%',
|
|
126
|
+
primaryDark: '217.2 91.2% 59.8%',
|
|
127
|
+
}),
|
|
128
|
+
buildTheme({
|
|
129
|
+
name: 'violet-creative',
|
|
130
|
+
title: 'Violet Creative',
|
|
131
|
+
description: '偏创意工具/设计产品的紫色主色',
|
|
132
|
+
baseColor: 'violet',
|
|
133
|
+
mood: 'creative premium',
|
|
134
|
+
bestFor: ['Creative Agency', 'Portfolio'],
|
|
135
|
+
primaryLight: '262.1 83.3% 57.8%',
|
|
136
|
+
primaryDark: '263.4 70% 50.4%',
|
|
137
|
+
}),
|
|
138
|
+
buildTheme({
|
|
139
|
+
name: 'rose-beauty',
|
|
140
|
+
title: 'Rose Beauty',
|
|
141
|
+
description: '美妆/生活方式产品的柔和玫瑰色',
|
|
142
|
+
baseColor: 'rose',
|
|
143
|
+
mood: 'soft elegant',
|
|
144
|
+
bestFor: ['Beauty', 'E-commerce'],
|
|
145
|
+
primaryLight: '346.8 77.2% 49.8%',
|
|
146
|
+
primaryDark: '346.8 77.2% 49.8%',
|
|
147
|
+
}),
|
|
148
|
+
buildTheme({
|
|
149
|
+
name: 'green-health',
|
|
150
|
+
title: 'Green Health',
|
|
151
|
+
description: '医疗/健康类产品的绿色主色',
|
|
152
|
+
baseColor: 'green',
|
|
153
|
+
mood: 'calm trustworthy',
|
|
154
|
+
bestFor: ['Healthcare App', 'Educational App'],
|
|
155
|
+
primaryLight: '142.1 76.2% 36.3%',
|
|
156
|
+
primaryDark: '142.1 70.6% 45.3%',
|
|
157
|
+
}),
|
|
158
|
+
buildTheme({
|
|
159
|
+
name: 'orange-energy',
|
|
160
|
+
title: 'Orange Energy',
|
|
161
|
+
description: '活动/增长类产品的橙色主色',
|
|
162
|
+
baseColor: 'orange',
|
|
163
|
+
mood: 'energetic friendly',
|
|
164
|
+
bestFor: ['Social Media App', 'Government/Public Service'],
|
|
165
|
+
primaryLight: '24.6 95% 53.1%',
|
|
166
|
+
primaryDark: '20.5 90.2% 48.2%',
|
|
167
|
+
}),
|
|
168
|
+
buildTheme({
|
|
169
|
+
name: 'stone-editorial',
|
|
170
|
+
title: 'Stone Editorial',
|
|
171
|
+
description: '内容/媒体站点常用的暖灰 editorial 感',
|
|
172
|
+
baseColor: 'stone',
|
|
173
|
+
mood: 'editorial warm',
|
|
174
|
+
bestFor: ['Portfolio/Personal', 'Educational App'],
|
|
175
|
+
primaryLight: '25 5.3% 24.7%',
|
|
176
|
+
primaryDark: '60 9.1% 97.8%',
|
|
177
|
+
}),
|
|
178
|
+
];
|
|
179
|
+
export function computeThemesChecksum(themes = CURATED_UI_THEMES) {
|
|
180
|
+
const signature = themes.map((theme) => `${theme.name}:${UI_THEMES_VERSION}`).join('|');
|
|
181
|
+
return crypto.createHash('sha256').update(signature).digest('hex').slice(0, 16);
|
|
182
|
+
}
|
|
183
|
+
export function syncThemesTo(outputDir, options) {
|
|
184
|
+
const checksum = computeThemesChecksum();
|
|
185
|
+
if (!options?.force && options?.existingChecksum === checksum) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const themesDir = path.join(outputDir, 'themes');
|
|
189
|
+
if (!fs.existsSync(themesDir)) {
|
|
190
|
+
fs.mkdirSync(themesDir, { recursive: true });
|
|
191
|
+
}
|
|
192
|
+
fs.writeFileSync(path.join(themesDir, 'presets.json'), `${JSON.stringify(CURATED_UI_THEMES, null, 2)}\n`, 'utf-8');
|
|
193
|
+
return {
|
|
194
|
+
metadata: {
|
|
195
|
+
version: UI_THEMES_VERSION,
|
|
196
|
+
syncedAt: new Date().toISOString(),
|
|
197
|
+
checksum,
|
|
198
|
+
},
|
|
199
|
+
count: CURATED_UI_THEMES.length,
|
|
200
|
+
};
|
|
201
|
+
}
|