sloth-d2c-mcp 1.0.4-beta65

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.
@@ -0,0 +1,165 @@
1
+ import { createServer } from 'net';
2
+ import { execSync } from 'child_process';
3
+ import axios from 'axios';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ /**
7
+ * 获取可用的端口号,从指定端口开始寻找
8
+ * @param startPort 起始端口号,默认3100
9
+ * @param maxAttempts 最大尝试次数,默认100
10
+ * @returns Promise<number> 返回可用的端口号
11
+ */
12
+ export async function getAvailablePort(startPort = 3100, maxAttempts = 10) {
13
+ for (let port = startPort; port < startPort + maxAttempts; port++) {
14
+ if (await isPortAvailable(port)) {
15
+ return port;
16
+ }
17
+ }
18
+ return 3100;
19
+ // throw new Error(`No available port found in range ${startPort}-${startPort + maxAttempts - 1}`)
20
+ }
21
+ /**
22
+ * 检查指定端口是否可用
23
+ * @param port 要检查的端口号
24
+ * @returns Promise<boolean> 端口是否可用
25
+ */
26
+ function isPortAvailable(port) {
27
+ return new Promise((resolve) => {
28
+ const server = createServer();
29
+ server.listen(port, () => {
30
+ server.close(() => {
31
+ resolve(true);
32
+ });
33
+ });
34
+ server.on('error', () => {
35
+ resolve(false);
36
+ });
37
+ });
38
+ }
39
+ // 判断是否全局可用的命令
40
+ export function isInstalled(command) {
41
+ try {
42
+ // macOS / Linux 用 which,Windows 用 where
43
+ const checkCmd = process.platform === 'win32' ? `where ${command}` : `which ${command}`;
44
+ execSync(checkCmd, { stdio: 'ignore' });
45
+ return true;
46
+ }
47
+ catch (error) {
48
+ return false;
49
+ }
50
+ }
51
+ export function getBase64(url) {
52
+ return axios
53
+ .get(url, {
54
+ responseType: 'arraybuffer',
55
+ })
56
+ .then((response) => Buffer.from(response.data, 'binary').toString('base64'));
57
+ }
58
+ /**
59
+ * Download Figma image and save it locally
60
+ * @param fileName - The filename to save as
61
+ * @param localPath - The local path to save to
62
+ * @param imageUrl - Image URL (images[nodeId])
63
+ * @returns A Promise that resolves to the full file path where the image was saved
64
+ * @throws Error if download fails
65
+ */
66
+ export async function downloadFigmaImage(fileName, localPath, imageUrl, base64) {
67
+ try {
68
+ console.log('downloadFigmaImage', 'fileName', fileName, 'localPath', localPath, 'imageUrl', imageUrl);
69
+ // 处理 fileName,提取文件名和可能的子目录路径
70
+ const fileNameParts = fileName.split('/');
71
+ const actualFileName = fileNameParts.pop() || fileName; // 获取最后的文件名部分
72
+ const subDirPath = fileNameParts.length > 0 ? fileNameParts.join('/') : ''; // 获取子目录路径
73
+ // 构建完整的本地路径,包含子目录
74
+ let fullLocalPath = localPath;
75
+ if (subDirPath) {
76
+ fullLocalPath = path.join(localPath, subDirPath);
77
+ }
78
+ // 确保目录存在
79
+ if (!fs.existsSync(fullLocalPath)) {
80
+ fs.mkdirSync(fullLocalPath, { recursive: true });
81
+ }
82
+ // 构建完整的文件路径
83
+ const fullPath = path.resolve(path.join(fullLocalPath, actualFileName));
84
+ console.log('downloadFigmaImage fullPath', fullPath);
85
+ if (base64) {
86
+ // 将base64字符串的前缀去掉,支持 data:image/png;base64, data:image/svg+xml;base64, 等格式
87
+ const base64Str = base64.replace(/^data:image\/[\w+]+;base64,/, '');
88
+ // 将base64写入为文件
89
+ const buffer = Buffer.from(base64Str, 'base64');
90
+ fs.writeFileSync(fullPath, buffer);
91
+ }
92
+ else {
93
+ // Use fetch to download the image
94
+ const response = await fetch(imageUrl, {
95
+ method: 'GET',
96
+ });
97
+ if (!response.ok) {
98
+ throw new Error(`HTTP error! status: ${response.status}`);
99
+ }
100
+ const buffer = await response.arrayBuffer();
101
+ fs.writeFileSync(fullPath, Buffer.from(buffer));
102
+ }
103
+ return fullPath;
104
+ }
105
+ catch (error) {
106
+ console.error('error', error);
107
+ const errorMessage = error instanceof Error ? error.message : String(error);
108
+ throw new Error(`Error downloading image: ${errorMessage}; url: ${imageUrl}`);
109
+ }
110
+ }
111
+ /**
112
+ * 获取系统支持的文件名
113
+ * @param name
114
+ * @returns
115
+ */
116
+ export const getSystemSupportFileName = (name) => {
117
+ return name.replace(/[\\\\/:*?\\"<>|#%]/g, '').replace(/\s+/g, '');
118
+ };
119
+ /**
120
+ * 保存图片文件并替换代码中的图片路径
121
+ * @param imageMap 图片映射对象
122
+ * @param code 原始代码
123
+ * @param root 根目录路径
124
+ * @returns 替换后的代码
125
+ */
126
+ export const saveImageFile = (params) => {
127
+ const { imageMap, root } = params;
128
+ // 下载图片文件
129
+ for (const [, source] of Object.entries(imageMap)) {
130
+ try {
131
+ downloadFigmaImage(source.path, path.join(root), source.src, source.base64);
132
+ }
133
+ catch (error) {
134
+ console.error('Failed to download image:', error);
135
+ }
136
+ }
137
+ };
138
+ export const replaceImageSrc = (code, imageMap) => {
139
+ let replacedCode = code;
140
+ for (const [, source] of Object.entries(imageMap)) {
141
+ if (!source?.src || !source?.path)
142
+ continue;
143
+ const escapedSrc = source.src?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
144
+ console.log('escapedSrc', escapedSrc);
145
+ if (escapedSrc) {
146
+ replacedCode = replacedCode.replace(new RegExp(escapedSrc, 'g'), source.path);
147
+ }
148
+ }
149
+ return replacedCode;
150
+ };
151
+ /**
152
+ * 重置节点列表的坐标
153
+ * @param nodeList 节点列表
154
+ * @returns
155
+ */
156
+ export const resetNodeListPosition = (nodeList) => {
157
+ console.log('resetNodeListPosition', nodeList);
158
+ const minX = Math.min(...nodeList.map((node) => node.x));
159
+ const minY = Math.min(...nodeList.map((node) => node.y));
160
+ return nodeList.map(({ ...node }) => {
161
+ node.x -= minX;
162
+ node.y -= minY;
163
+ return node;
164
+ });
165
+ };
@@ -0,0 +1,133 @@
1
+ import * as net from 'net';
2
+ /**
3
+ * VSCode 日志传输器
4
+ * 将日志通过 TCP 连接发送到 VSCode 扩展的输出面板
5
+ */
6
+ export class VSCodeLogger {
7
+ static instance;
8
+ socket = null;
9
+ isConnected = false;
10
+ reconnectTimer = null;
11
+ logBuffer = [];
12
+ maxBufferSize = 100;
13
+ constructor() {
14
+ this.connect();
15
+ }
16
+ static getInstance() {
17
+ if (!VSCodeLogger.instance) {
18
+ VSCodeLogger.instance = new VSCodeLogger();
19
+ }
20
+ return VSCodeLogger.instance;
21
+ }
22
+ connect() {
23
+ try {
24
+ this.socket = new net.Socket();
25
+ this.socket.connect(13142, '127.0.0.1', () => {
26
+ this.isConnected = true;
27
+ console.log('[VSCodeLogger] 已连接到 VSCode 扩展日志服务');
28
+ // 发送缓冲的日志
29
+ this.flushBuffer();
30
+ // 清除重连定时器
31
+ if (this.reconnectTimer) {
32
+ clearTimeout(this.reconnectTimer);
33
+ this.reconnectTimer = null;
34
+ }
35
+ });
36
+ this.socket.on('error', (error) => {
37
+ this.isConnected = false;
38
+ console.log('[VSCodeLogger] 连接错误:', error.message);
39
+ this.scheduleReconnect();
40
+ });
41
+ this.socket.on('close', () => {
42
+ this.isConnected = false;
43
+ console.log('[VSCodeLogger] 连接已关闭');
44
+ this.scheduleReconnect();
45
+ });
46
+ }
47
+ catch (error) {
48
+ console.log('[VSCodeLogger] 连接失败:', error);
49
+ this.scheduleReconnect();
50
+ }
51
+ }
52
+ scheduleReconnect() {
53
+ if (this.reconnectTimer) {
54
+ return; // 已经在重连中
55
+ }
56
+ this.reconnectTimer = setTimeout(() => {
57
+ console.log('[VSCodeLogger] 尝试重新连接...');
58
+ this.connect();
59
+ }, 5000); // 5秒后重连
60
+ }
61
+ flushBuffer() {
62
+ if (this.logBuffer.length > 0 && this.isConnected && this.socket) {
63
+ const logs = this.logBuffer.join('');
64
+ this.socket.write(logs);
65
+ this.logBuffer = [];
66
+ }
67
+ }
68
+ sendLog(level, ...args) {
69
+ const timestamp = new Date().toISOString();
70
+ const levelPrefix = level.toUpperCase().padEnd(5);
71
+ const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)).join(' ');
72
+ const logLine = `[${timestamp}] [${levelPrefix}] ${message}\n`;
73
+ if (this.isConnected && this.socket) {
74
+ try {
75
+ this.socket.write(logLine);
76
+ }
77
+ catch (error) {
78
+ // 如果发送失败,添加到缓冲区
79
+ this.addToBuffer(logLine);
80
+ }
81
+ }
82
+ else {
83
+ // 连接未建立,添加到缓冲区
84
+ this.addToBuffer(logLine);
85
+ }
86
+ }
87
+ addToBuffer(logLine) {
88
+ this.logBuffer.push(logLine);
89
+ // 限制缓冲区大小
90
+ if (this.logBuffer.length > this.maxBufferSize) {
91
+ this.logBuffer.shift(); // 移除最旧的日志
92
+ }
93
+ }
94
+ log(...args) {
95
+ this.sendLog('log', ...args);
96
+ }
97
+ error(...args) {
98
+ this.sendLog('error', ...args);
99
+ }
100
+ warn(...args) {
101
+ this.sendLog('warn', ...args);
102
+ }
103
+ info(...args) {
104
+ this.sendLog('info', ...args);
105
+ }
106
+ reconnect() {
107
+ console.log('[VSCodeLogger] 手动重连请求');
108
+ // 清除现有连接和定时器
109
+ if (this.reconnectTimer) {
110
+ clearTimeout(this.reconnectTimer);
111
+ this.reconnectTimer = null;
112
+ }
113
+ if (this.socket) {
114
+ this.socket.destroy();
115
+ this.socket = null;
116
+ }
117
+ this.isConnected = false;
118
+ // 立即重新连接
119
+ this.connect();
120
+ }
121
+ destroy() {
122
+ if (this.reconnectTimer) {
123
+ clearTimeout(this.reconnectTimer);
124
+ this.reconnectTimer = null;
125
+ }
126
+ if (this.socket) {
127
+ this.socket.destroy();
128
+ this.socket = null;
129
+ }
130
+ this.isConnected = false;
131
+ this.logBuffer = [];
132
+ }
133
+ }
@@ -0,0 +1,196 @@
1
+ // @ts-nocheck
2
+ const parseResource = (str) => {
3
+ return {
4
+ resource: str,
5
+ path: str,
6
+ query: '',
7
+ fragment: '',
8
+ };
9
+ };
10
+ const REGEXP = /\[\\*([\w:]+)\\*\]/gi;
11
+ /**
12
+ * @param {string | number} id id
13
+ * @returns {string | number} result
14
+ */
15
+ const prepareId = (id) => {
16
+ if (typeof id !== 'string')
17
+ return id;
18
+ if (/^"\s\+*.*\+\s*"$/.test(id)) {
19
+ const match = /^"\s\+*\s*(.*)\s*\+\s*"$/.exec(id);
20
+ return `" + (${ /** @type {string[]} */match[1]} + "").replace(/(^[.-]|[^a-zA-Z0-9_-])+/g, "_") + "`;
21
+ }
22
+ return id.replace(/(^[.-]|[^a-zA-Z0-9_-])+/g, '_');
23
+ };
24
+ /**
25
+ * @callback ReplacerFunction
26
+ * @param {string} match
27
+ * @param {string | undefined} arg
28
+ * @param {string} input
29
+ */
30
+ /**
31
+ * @param {ReplacerFunction} replacer replacer
32
+ * @param {((arg0: number) => string) | undefined} handler handler
33
+ * @param {AssetInfo | undefined} assetInfo asset info
34
+ * @param {string} hashName hash name
35
+ * @returns {ReplacerFunction} hash replacer function
36
+ */
37
+ const hashLength = (replacer, handler, assetInfo, hashName) => {
38
+ /** @type {ReplacerFunction} */
39
+ const fn = (match, arg, input) => {
40
+ let result;
41
+ const length = arg && Number.parseInt(arg, 10);
42
+ if (length && handler) {
43
+ result = handler(length);
44
+ }
45
+ else {
46
+ const hash = replacer(match, arg, input);
47
+ result = length ? hash.slice(0, length) : hash;
48
+ }
49
+ if (assetInfo) {
50
+ assetInfo.immutable = true;
51
+ if (Array.isArray(assetInfo[hashName])) {
52
+ assetInfo[hashName] = [...assetInfo[hashName], result];
53
+ }
54
+ else if (assetInfo[hashName]) {
55
+ assetInfo[hashName] = [assetInfo[hashName], result];
56
+ }
57
+ else {
58
+ assetInfo[hashName] = result;
59
+ }
60
+ }
61
+ return result;
62
+ };
63
+ return fn;
64
+ };
65
+ /** @typedef {(match: string, arg?: string, input?: string) => string} Replacer */
66
+ /**
67
+ * @param {string | number | null | undefined | (() => string | number | null | undefined)} value value
68
+ * @param {boolean=} allowEmpty allow empty
69
+ * @returns {Replacer} replacer
70
+ */
71
+ const replacer = (value, allowEmpty) => {
72
+ /** @type {Replacer} */
73
+ const fn = (match, arg, input) => {
74
+ if (typeof value === 'function') {
75
+ value = value();
76
+ }
77
+ if (value === null || value === undefined) {
78
+ if (!allowEmpty) {
79
+ throw new Error(`Path variable ${match} not implemented in this context: ${input}`);
80
+ }
81
+ return '';
82
+ }
83
+ return `${value}`;
84
+ };
85
+ return fn;
86
+ };
87
+ /** @typedef {string | function(PathData, AssetInfo=): string} TemplatePath */
88
+ /**
89
+ * @param {TemplatePath} path the raw path
90
+ * @param {PathData} data context data
91
+ * @param {AssetInfo | undefined} assetInfo extra info about the asset (will be written to)
92
+ * @returns {string} the interpolated path
93
+ */
94
+ export const replacePathVariables = (path, data, assetInfo) => {
95
+ /** @type {Map<string, Function>} */
96
+ const replacements = new Map();
97
+ // Filename context
98
+ //
99
+ // Placeholders
100
+ //
101
+ // for /some/path/file.js?query#fragment:
102
+ // [file] - /some/path/file.js
103
+ // [query] - ?query
104
+ // [fragment] - #fragment
105
+ // [base] - file.js
106
+ // [path] - /some/path/
107
+ // [name] - file
108
+ // [ext] - .js
109
+ if (typeof data.filename === 'string') {
110
+ // check that filename is data uri
111
+ const match = data.filename.match(/^data:([^;,]+)/);
112
+ if (match) {
113
+ const emptyReplacer = replacer('', true);
114
+ // "XXXX" used for `updateHash`, so we don't need it here
115
+ const contentHash = data.contentHash && !/X+/.test(data.contentHash) ? data.contentHash : false;
116
+ const baseReplacer = contentHash ? replacer(contentHash) : emptyReplacer;
117
+ replacements.set('file', emptyReplacer);
118
+ replacements.set('query', emptyReplacer);
119
+ replacements.set('path', emptyReplacer);
120
+ replacements.set('base', baseReplacer);
121
+ replacements.set('name', baseReplacer);
122
+ }
123
+ else {
124
+ const { path: file, query, fragment } = parseResource(data.filename);
125
+ const ext = file.includes('.') ? file.substring(file.lastIndexOf('.') + 1) : '';
126
+ const base = file.split('/').pop() || '';
127
+ const name = base.slice(0, base.length - ext.length);
128
+ const path = file.slice(0, file.length - base.length);
129
+ replacements.set('file', replacer(file));
130
+ replacements.set('query', replacer(query, true));
131
+ replacements.set('path', replacer(path, true));
132
+ replacements.set('base', replacer(base));
133
+ replacements.set('name', replacer(name));
134
+ replacements.set('ext', replacer(ext, true));
135
+ }
136
+ }
137
+ // Compilation context
138
+ //
139
+ // Placeholders
140
+ //
141
+ // [fullhash] - data.hash (3a4b5c6e7f)
142
+ //
143
+ // Legacy Placeholders
144
+ //
145
+ // [hash] - data.hash (3a4b5c6e7f)
146
+ if (data.hash) {
147
+ const hashReplacer = hashLength(replacer(data.hash), data.hashWithLength, assetInfo, 'fullhash');
148
+ replacements.set('fullhash', hashReplacer);
149
+ }
150
+ // Chunk Context
151
+ //
152
+ // Placeholders
153
+ //
154
+ // [id] - chunk.id (0.js)
155
+ // [name] - chunk.name (app.js)
156
+ // [chunkhash] - chunk.hash (7823t4t4.js)
157
+ // [contenthash] - chunk.contentHash[type] (3256u3zg.js)
158
+ if (data.chunk) {
159
+ const chunk = data.chunk;
160
+ const contentHashType = data.contentHashType;
161
+ const idReplacer = replacer(chunk.id);
162
+ const nameReplacer = replacer(chunk.name || chunk.id);
163
+ const contenthashReplacer = hashLength(replacer(data.contentHash || (contentHashType && chunk.contentHash && chunk.contentHash[contentHashType])), data.contentHashWithLength ||
164
+ ('contentHashWithLength' in chunk && chunk.contentHashWithLength ? chunk.contentHashWithLength[ /** @type {string} */contentHashType] : undefined), assetInfo, 'contenthash');
165
+ replacements.set('id', idReplacer);
166
+ replacements.set('name', nameReplacer);
167
+ replacements.set('contenthash', contenthashReplacer);
168
+ }
169
+ // Other things
170
+ if (data.url) {
171
+ replacements.set('url', replacer(data.url));
172
+ }
173
+ if (typeof data.runtime === 'string') {
174
+ replacements.set('runtime', replacer(() => prepareId(/** @type {string} */ data.runtime)));
175
+ }
176
+ else {
177
+ replacements.set('runtime', replacer('_'));
178
+ }
179
+ path = path.replace(REGEXP, (match, content) => {
180
+ if (content.length + 2 === match.length) {
181
+ const contentMatch = /^(\w+)(?::(\w+))?$/.exec(content);
182
+ if (!contentMatch)
183
+ return match;
184
+ const [, kind, arg] = contentMatch;
185
+ const replacer = replacements.get(kind);
186
+ if (replacer !== undefined) {
187
+ return replacer(match, arg, path);
188
+ }
189
+ }
190
+ else if (match.startsWith('[\\') && match.endsWith('\\]')) {
191
+ return `[${match.slice(2, -2)}]`;
192
+ }
193
+ return match;
194
+ });
195
+ return path;
196
+ };
@@ -0,0 +1,18 @@
1
+ {
2
+ "buildTime": "2025-10-21T12:55:46.930Z",
3
+ "mode": "build",
4
+ "pages": {
5
+ "main": {
6
+ "file": "index.html",
7
+ "size": 1089770,
8
+ "sizeFormatted": "1.04 MB"
9
+ },
10
+ "detail": {
11
+ "file": "detail.html",
12
+ "size": 240085,
13
+ "sizeFormatted": "234.46 KB"
14
+ }
15
+ },
16
+ "totalSize": 1329855,
17
+ "totalSizeFormatted": "1.27 MB"
18
+ }