@hangox/mg-cli 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.
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 错误处理测试
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+ import {
7
+ ErrorCode,
8
+ ErrorNames,
9
+ ErrorMessages,
10
+ MGError,
11
+ createError,
12
+ } from '../../../src/shared/errors.js'
13
+
14
+ describe('ErrorCode', () => {
15
+ it('应该包含所有预定义错误码', () => {
16
+ expect(ErrorCode.CONNECTION_FAILED).toBe('E001')
17
+ expect(ErrorCode.NODE_NOT_FOUND).toBe('E005')
18
+ expect(ErrorCode.INVALID_LINK).toBe('E010')
19
+ expect(ErrorCode.SERVER_ALREADY_RUNNING).toBe('E016')
20
+ })
21
+ })
22
+
23
+ describe('ErrorNames', () => {
24
+ it('应该为每个错误码提供名称', () => {
25
+ expect(ErrorNames[ErrorCode.CONNECTION_FAILED]).toBe('CONNECTION_FAILED')
26
+ expect(ErrorNames[ErrorCode.NODE_NOT_FOUND]).toBe('NODE_NOT_FOUND')
27
+ })
28
+ })
29
+
30
+ describe('ErrorMessages', () => {
31
+ it('应该为每个错误码提供默认消息', () => {
32
+ expect(ErrorMessages[ErrorCode.CONNECTION_FAILED]).toBe('无法连接到 MG Server')
33
+ expect(ErrorMessages[ErrorCode.NODE_NOT_FOUND]).toBe('节点不存在')
34
+ })
35
+ })
36
+
37
+ describe('MGError', () => {
38
+ it('应该创建带有默认消息的错误', () => {
39
+ const error = new MGError(ErrorCode.NODE_NOT_FOUND)
40
+ expect(error.code).toBe('E005')
41
+ expect(error.errorName).toBe('NODE_NOT_FOUND')
42
+ expect(error.message).toBe('节点不存在')
43
+ })
44
+
45
+ it('应该创建带有自定义消息的错误', () => {
46
+ const error = new MGError(ErrorCode.NODE_NOT_FOUND, '节点 123:456 不存在')
47
+ expect(error.message).toBe('节点 123:456 不存在')
48
+ })
49
+
50
+ it('应该支持 details', () => {
51
+ const error = new MGError(ErrorCode.NODE_NOT_FOUND, '节点不存在', {
52
+ nodeId: '123:456',
53
+ })
54
+ expect(error.details).toEqual({ nodeId: '123:456' })
55
+ })
56
+
57
+ it('应该正确序列化为 JSON', () => {
58
+ const error = new MGError(ErrorCode.NODE_NOT_FOUND, '节点不存在', {
59
+ nodeId: '123:456',
60
+ })
61
+ const json = error.toJSON()
62
+ expect(json).toEqual({
63
+ code: 'E005',
64
+ name: 'NODE_NOT_FOUND',
65
+ message: '节点不存在',
66
+ details: { nodeId: '123:456' },
67
+ })
68
+ })
69
+
70
+ it('应该正确转换为字符串', () => {
71
+ const error = new MGError(ErrorCode.NODE_NOT_FOUND)
72
+ expect(error.toString()).toBe('错误 [E005]: 节点不存在')
73
+ })
74
+ })
75
+
76
+ describe('createError', () => {
77
+ it('应该创建 MGError 实例', () => {
78
+ const error = createError(ErrorCode.INVALID_LINK)
79
+ expect(error).toBeInstanceOf(MGError)
80
+ expect(error.code).toBe('E010')
81
+ })
82
+ })
@@ -0,0 +1,208 @@
1
+ /**
2
+ * 工具函数测试
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+ import {
7
+ normalizePageUrl,
8
+ isDesignPageUrl,
9
+ parseMgpLink,
10
+ generateMgpLink,
11
+ formatFileSize,
12
+ formatDuration,
13
+ generateId,
14
+ extractFileId,
15
+ extractFileIdFromUrl,
16
+ extractFileIdFromMgpLink,
17
+ } from '../../../src/shared/utils.js'
18
+
19
+ describe('normalizePageUrl', () => {
20
+ it('应该标准化完整 URL', () => {
21
+ const url =
22
+ 'https://mastergo.netease.com/file/174135798361888?fileOpenFrom=home&page_id=0%3A8347'
23
+ const result = normalizePageUrl(url)
24
+ expect(result).toBe('mastergo.netease.com/file/174135798361888')
25
+ })
26
+
27
+ it('应该处理不带查询参数的 URL', () => {
28
+ const url = 'https://mastergo.netease.com/file/174135798361888'
29
+ const result = normalizePageUrl(url)
30
+ expect(result).toBe('mastergo.netease.com/file/174135798361888')
31
+ })
32
+
33
+ it('应该处理已标准化的 URL', () => {
34
+ const url = 'mastergo.netease.com/file/174135798361888'
35
+ const result = normalizePageUrl(url)
36
+ expect(result).toBe('mastergo.netease.com/file/174135798361888')
37
+ })
38
+
39
+ it('应该处理 mastergo.com 域名', () => {
40
+ const url = 'https://mastergo.com/file/123456'
41
+ const result = normalizePageUrl(url)
42
+ expect(result).toBe('mastergo.com/file/123456')
43
+ })
44
+ })
45
+
46
+ describe('isDesignPageUrl', () => {
47
+ it('应该识别设计页面 URL', () => {
48
+ expect(isDesignPageUrl('/file/174135798361888')).toBe(true)
49
+ expect(isDesignPageUrl('mastergo.com/file/123456')).toBe(true)
50
+ })
51
+
52
+ it('应该拒绝非设计页面 URL', () => {
53
+ expect(isDesignPageUrl('/files/home')).toBe(false)
54
+ expect(isDesignPageUrl('/files/recent')).toBe(false)
55
+ expect(isDesignPageUrl('/files/trash')).toBe(false)
56
+ })
57
+ })
58
+
59
+ describe('parseMgpLink', () => {
60
+ it('应该解析有效的 mgp:// 链接', () => {
61
+ const link = 'mgp://mastergo.netease.com/file/174135798361888?nodeId=123%3A456'
62
+ const result = parseMgpLink(link)
63
+ expect(result).toEqual({
64
+ pageUrl: 'mastergo.netease.com/file/174135798361888',
65
+ nodeId: '123:456',
66
+ })
67
+ })
68
+
69
+ it('应该解析 mastergo.com 域名的链接', () => {
70
+ const link = 'mgp://mastergo.com/file/123456?nodeId=789%3A101'
71
+ const result = parseMgpLink(link)
72
+ expect(result).toEqual({
73
+ pageUrl: 'mastergo.com/file/123456',
74
+ nodeId: '789:101',
75
+ })
76
+ })
77
+
78
+ it('应该解析带父节点路径的链接', () => {
79
+ const link = 'mgp://mastergo.netease.com/file/174875497054651?nodeId=0%3A8633&nodePath=314%3A13190%2F0%3A8633'
80
+ const result = parseMgpLink(link)
81
+ expect(result).toEqual({
82
+ pageUrl: 'mastergo.netease.com/file/174875497054651',
83
+ nodeId: '0:8633',
84
+ nodePath: ['314:13190', '0:8633'],
85
+ })
86
+ })
87
+
88
+ it('应该解析多层父节点路径的链接', () => {
89
+ const link = 'mgp://mastergo.netease.com/file/123?nodeId=3%3A3&nodePath=1%3A1%2F2%3A2%2F3%3A3'
90
+ const result = parseMgpLink(link)
91
+ expect(result).toEqual({
92
+ pageUrl: 'mastergo.netease.com/file/123',
93
+ nodeId: '3:3',
94
+ nodePath: ['1:1', '2:2', '3:3'],
95
+ })
96
+ })
97
+
98
+ it('应该解析 nodeId 包含路径格式的链接', () => {
99
+ // 用户直接复制的链接,nodeId 中包含路径
100
+ const link = 'mgp://mastergo.netease.com/file/174875497054651?nodeId=314%3A24807%2F0%3A3443'
101
+ const result = parseMgpLink(link)
102
+ expect(result).toEqual({
103
+ pageUrl: 'mastergo.netease.com/file/174875497054651',
104
+ nodeId: '314:24807/0:3443',
105
+ })
106
+ })
107
+
108
+ it('应该拒绝无效的链接', () => {
109
+ // 不是 mgp:// 协议
110
+ expect(parseMgpLink('https://mastergo.com/file/123?nodeId=1%3A2')).toBeNull()
111
+ // 没有 nodeId 参数
112
+ expect(parseMgpLink('mgp://mastergo.com/file/123')).toBeNull()
113
+ // nodeId 格式不对(没有编码)
114
+ expect(parseMgpLink('mgp://mastergo.com/file/123?nodeId=invalid')).toBeNull()
115
+ // 完全无效
116
+ expect(parseMgpLink('invalid')).toBeNull()
117
+ // 没有查询参数
118
+ expect(parseMgpLink('mgp://custom.domain.com/design/abc123')).toBeNull()
119
+ })
120
+ })
121
+
122
+ describe('generateMgpLink', () => {
123
+ it('应该生成正确的 mgp:// 链接', () => {
124
+ const pageUrl = 'https://mastergo.netease.com/file/174135798361888'
125
+ const nodeId = '123:456'
126
+ const result = generateMgpLink(pageUrl, nodeId)
127
+ expect(result).toBe('mgp://mastergo.netease.com/file/174135798361888?nodeId=123%3A456')
128
+ })
129
+
130
+ it('应该生成带 nodePath 的链接', () => {
131
+ const pageUrl = 'https://mastergo.netease.com/file/123'
132
+ const nodeId = '3:3'
133
+ const nodePath = ['1:1', '2:2', '3:3']
134
+ const result = generateMgpLink(pageUrl, nodeId, nodePath)
135
+ expect(result).toBe('mgp://mastergo.netease.com/file/123?nodeId=3%3A3&nodePath=1%3A1%2F2%3A2%2F3%3A3')
136
+ })
137
+ })
138
+
139
+ describe('formatFileSize', () => {
140
+ it('应该格式化字节大小', () => {
141
+ expect(formatFileSize(500)).toBe('500 字节')
142
+ expect(formatFileSize(1024)).toBe('1.00 KB')
143
+ expect(formatFileSize(1536)).toBe('1.50 KB')
144
+ expect(formatFileSize(1048576)).toBe('1.00 MB')
145
+ })
146
+ })
147
+
148
+ describe('formatDuration', () => {
149
+ it('应该格式化时间间隔', () => {
150
+ expect(formatDuration(5000)).toBe('5 秒')
151
+ expect(formatDuration(65000)).toBe('1 分钟 5 秒')
152
+ expect(formatDuration(3665000)).toBe('1 小时 1 分钟')
153
+ expect(formatDuration(90000000)).toBe('1 天 1 小时')
154
+ })
155
+ })
156
+
157
+ describe('generateId', () => {
158
+ it('应该生成唯一 ID', () => {
159
+ const id1 = generateId()
160
+ const id2 = generateId()
161
+ expect(id1).not.toBe(id2)
162
+ expect(id1).toMatch(/^\d+_[a-z0-9]+$/)
163
+ })
164
+ })
165
+
166
+ describe('extractFileIdFromUrl', () => {
167
+ it('应该从完整 URL 提取 fileId', () => {
168
+ expect(extractFileIdFromUrl('https://mastergo.netease.com/file/174875497054651?page_id=321%3A11979')).toBe('174875497054651')
169
+ expect(extractFileIdFromUrl('https://mastergo.netease.com/file/123456')).toBe('123456')
170
+ expect(extractFileIdFromUrl('mastergo.netease.com/file/789')).toBe('789')
171
+ })
172
+
173
+ it('应该处理无效 URL', () => {
174
+ expect(extractFileIdFromUrl('https://mastergo.netease.com/files/home')).toBeNull()
175
+ expect(extractFileIdFromUrl('invalid')).toBeNull()
176
+ })
177
+ })
178
+
179
+ describe('extractFileIdFromMgpLink', () => {
180
+ it('应该从 mgp:// 链接提取 fileId', () => {
181
+ expect(extractFileIdFromMgpLink('mgp://mastergo.netease.com/file/174875497054651?nodeId=xxx')).toBe('174875497054651')
182
+ })
183
+
184
+ it('应该拒绝非 mgp:// 链接', () => {
185
+ expect(extractFileIdFromMgpLink('https://mastergo.netease.com/file/123')).toBeNull()
186
+ })
187
+ })
188
+
189
+ describe('extractFileId', () => {
190
+ it('应该处理纯数字 fileId', () => {
191
+ expect(extractFileId('174875497054651')).toBe('174875497054651')
192
+ expect(extractFileId(' 123456 ')).toBe('123456')
193
+ })
194
+
195
+ it('应该处理完整 URL', () => {
196
+ expect(extractFileId('https://mastergo.netease.com/file/174875497054651?page_id=xxx')).toBe('174875497054651')
197
+ })
198
+
199
+ it('应该处理 mgp:// 协议', () => {
200
+ expect(extractFileId('mgp://mastergo.netease.com/file/174875497054651?nodeId=xxx')).toBe('174875497054651')
201
+ })
202
+
203
+ it('应该处理无效输入', () => {
204
+ expect(extractFileId('invalid')).toBeNull()
205
+ expect(extractFileId('https://example.com')).toBeNull()
206
+ })
207
+ // 需要先导入
208
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "noEmit": true
19
+ },
20
+ "include": ["src/**/*"],
21
+ "exclude": ["node_modules", "dist", "tests"]
22
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig([
4
+ // 主模块(不需要 shebang)
5
+ {
6
+ entry: {
7
+ index: 'src/index.ts',
8
+ server: 'src/server/index.ts',
9
+ },
10
+ format: ['esm'],
11
+ dts: true,
12
+ clean: true,
13
+ sourcemap: true,
14
+ target: 'node20',
15
+ splitting: false,
16
+ },
17
+ // CLI 入口(需要 shebang)
18
+ {
19
+ entry: {
20
+ cli: 'src/cli/index.ts',
21
+ 'daemon-runner': 'src/server/daemon-runner.ts',
22
+ },
23
+ format: ['esm'],
24
+ dts: false,
25
+ clean: false, // 不清理,避免覆盖上面的输出
26
+ sourcemap: true,
27
+ target: 'node20',
28
+ splitting: false,
29
+ banner: {
30
+ js: '#!/usr/bin/env node',
31
+ },
32
+ },
33
+ ])
@@ -0,0 +1,22 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['tests/**/*.test.ts'],
8
+ coverage: {
9
+ provider: 'v8',
10
+ reporter: ['text', 'json', 'html'],
11
+ exclude: ['tests/**', 'dist/**', '*.config.*'],
12
+ thresholds: {
13
+ lines: 80,
14
+ functions: 80,
15
+ branches: 75,
16
+ statements: 80,
17
+ },
18
+ },
19
+ testTimeout: 10000,
20
+ hookTimeout: 10000,
21
+ },
22
+ })