codebuddy-stats 1.0.0 → 1.1.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,235 @@
1
+ import fs from 'node:fs/promises'
2
+ import fsSync from 'node:fs'
3
+ import path from 'node:path'
4
+ import os from 'node:os'
5
+ import crypto from 'node:crypto'
6
+
7
+ import { getWorkspaceStorageDir } from './paths.js'
8
+
9
+ export interface WorkspaceMapping {
10
+ hash: string
11
+ folderUri: string
12
+ displayPath: string
13
+ }
14
+
15
+ // 缓存已解析的 CodeBuddy Code 路径名
16
+ const codePathCache = new Map<string, string>()
17
+
18
+ /**
19
+ * 从 folder URI 提取纯路径(用于计算 MD5)
20
+ */
21
+ function extractPathFromUri(folderUri: string): string | null {
22
+ // 处理本地文件路径: file:///path/to/folder
23
+ if (folderUri.startsWith('file://')) {
24
+ try {
25
+ const url = new URL(folderUri)
26
+ return decodeURIComponent(url.pathname)
27
+ } catch {
28
+ return decodeURIComponent(folderUri.replace('file://', ''))
29
+ }
30
+ }
31
+
32
+ // 处理远程路径: vscode-remote://codebuddy-remote-ssh%2B.../path
33
+ if (folderUri.startsWith('vscode-remote://')) {
34
+ try {
35
+ const url = new URL(folderUri)
36
+ return decodeURIComponent(url.pathname)
37
+ } catch {
38
+ const match = folderUri.match(/vscode-remote:\/\/[^/]+(.+)$/)
39
+ if (match?.[1]) {
40
+ return decodeURIComponent(match[1])
41
+ }
42
+ }
43
+ }
44
+
45
+ return null
46
+ }
47
+
48
+ /**
49
+ * 格式化路径用于显示(简化 home 目录)
50
+ */
51
+ function formatDisplayPath(p: string): string {
52
+ const home = os.homedir()
53
+ if (p.startsWith(home)) {
54
+ return '~' + p.slice(home.length)
55
+ }
56
+ return p
57
+ }
58
+
59
+ /**
60
+ * 从 folder URI 生成用于显示的友好路径
61
+ */
62
+ function getDisplayPath(folderUri: string): string {
63
+ // 本地路径
64
+ if (folderUri.startsWith('file://')) {
65
+ const p = extractPathFromUri(folderUri)
66
+ if (p) {
67
+ return formatDisplayPath(p)
68
+ }
69
+ }
70
+
71
+ // 远程路径
72
+ if (folderUri.startsWith('vscode-remote://')) {
73
+ const p = extractPathFromUri(folderUri)
74
+ if (p) {
75
+ const hostMatch = folderUri.match(/vscode-remote:\/\/codebuddy-remote-ssh%2B([^/]+)/)
76
+ if (hostMatch?.[1]) {
77
+ let host = decodeURIComponent(hostMatch[1])
78
+ host = host.replace(/_x([0-9a-fA-F]{2})_/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
79
+ host = host.replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
80
+ if (host.includes('@')) {
81
+ const parts = host.split('@')
82
+ host = parts[parts.length - 1]?.split(':')[0] || host
83
+ }
84
+ if (host.length > 20) {
85
+ host = host.slice(0, 17) + '...'
86
+ }
87
+ return `[${host}]${p}`
88
+ }
89
+ return `[remote]${p}`
90
+ }
91
+ }
92
+
93
+ return folderUri
94
+ }
95
+
96
+ /**
97
+ * 计算路径的 MD5 hash(CodeBuddyExtension 使用纯路径计算)
98
+ */
99
+ function computePathHash(p: string): string {
100
+ return crypto.createHash('md5').update(p).digest('hex')
101
+ }
102
+
103
+ /**
104
+ * 加载所有工作区映射
105
+ */
106
+ export async function loadWorkspaceMappings(): Promise<Map<string, WorkspaceMapping>> {
107
+ const mappings = new Map<string, WorkspaceMapping>()
108
+ const storageDir = getWorkspaceStorageDir()
109
+
110
+ let entries: fsSync.Dirent[] = []
111
+ try {
112
+ entries = await fs.readdir(storageDir, { withFileTypes: true })
113
+ } catch {
114
+ return mappings
115
+ }
116
+
117
+ for (const entry of entries) {
118
+ if (!entry.isDirectory()) continue
119
+
120
+ const workspaceJsonPath = path.join(storageDir, entry.name, 'workspace.json')
121
+ try {
122
+ const content = await fs.readFile(workspaceJsonPath, 'utf8')
123
+ const data = JSON.parse(content) as { folder?: string }
124
+ const folderUri = data.folder
125
+ if (!folderUri) continue
126
+
127
+ const extractedPath = extractPathFromUri(folderUri)
128
+ if (!extractedPath) continue
129
+
130
+ const hash = computePathHash(extractedPath)
131
+ const displayPath = getDisplayPath(folderUri)
132
+
133
+ mappings.set(hash, { hash, folderUri, displayPath })
134
+ } catch {
135
+ // 跳过无法读取的文件
136
+ }
137
+ }
138
+
139
+ return mappings
140
+ }
141
+
142
+ /**
143
+ * 检查路径是否存在(同步版本,用于路径探测)
144
+ */
145
+ function pathExistsSync(p: string): boolean {
146
+ try {
147
+ fsSync.accessSync(p)
148
+ return true
149
+ } catch {
150
+ return false
151
+ }
152
+ }
153
+
154
+ /**
155
+ * 尝试将 CodeBuddy Code 的项目名(路径中 / 替换为 -)还原为真实路径
156
+ * 使用回溯搜索,因为目录名本身可能包含 -
157
+ *
158
+ * 例如: "Users-anoti-Documents-project-codebudy-cost-analyzer"
159
+ * -> "/Users/anoti/Documents/project/codebudy-cost-analyzer"
160
+ */
161
+ function tryResolveCodePath(name: string): string | null {
162
+ // 检查缓存
163
+ const cached = codePathCache.get(name)
164
+ if (cached !== undefined) {
165
+ return cached || null
166
+ }
167
+
168
+ const parts = name.split('-')
169
+ if (parts.length < 2) {
170
+ codePathCache.set(name, '')
171
+ return null
172
+ }
173
+
174
+ // 回溯搜索:尝试不同的分割方式
175
+ function backtrack(index: number, currentPath: string): string | null {
176
+ if (index >= parts.length) {
177
+ // 检查完整路径是否存在
178
+ if (pathExistsSync(currentPath)) {
179
+ return currentPath
180
+ }
181
+ return null
182
+ }
183
+
184
+ // 尝试从当前位置开始,合并不同数量的 parts
185
+ for (let end = index; end < parts.length; end++) {
186
+ const segment = parts.slice(index, end + 1).join('-')
187
+ const newPath = currentPath ? `${currentPath}/${segment}` : `/${segment}`
188
+
189
+ // 如果这不是最后一段,检查目录是否存在
190
+ if (end < parts.length - 1) {
191
+ if (pathExistsSync(newPath)) {
192
+ const result = backtrack(end + 1, newPath)
193
+ if (result) return result
194
+ }
195
+ } else {
196
+ // 最后一段,检查完整路径
197
+ if (pathExistsSync(newPath)) {
198
+ return newPath
199
+ }
200
+ }
201
+ }
202
+
203
+ return null
204
+ }
205
+
206
+ const result = backtrack(0, '')
207
+ codePathCache.set(name, result || '')
208
+ return result
209
+ }
210
+
211
+ /**
212
+ * 解析项目名称
213
+ * - MD5 hash (32位十六进制): 从 IDE workspaceMappings 查找
214
+ * - 路径格式 (包含 -): 尝试还原 CodeBuddy Code 的路径格式
215
+ */
216
+ export function resolveProjectName(name: string, mappings?: Map<string, WorkspaceMapping>): string {
217
+ // IDE source: MD5 hash
218
+ if (mappings && /^[a-f0-9]{32}$/.test(name)) {
219
+ const mapping = mappings.get(name)
220
+ if (mapping) {
221
+ return mapping.displayPath
222
+ }
223
+ }
224
+
225
+ // Code source: 路径中 / 替换为 - 的格式
226
+ // 特征:以大写字母开头(如 Users-、home-),包含 -
227
+ if (/^[A-Za-z]/.test(name) && name.includes('-')) {
228
+ const resolved = tryResolveCodePath(name)
229
+ if (resolved) {
230
+ return formatDisplayPath(resolved)
231
+ }
232
+ }
233
+
234
+ return name
235
+ }
package/index.js DELETED
@@ -1,16 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // ESM wrapper entrypoint (kept for backward compatibility)
4
- // If you run this file directly, it will execute the compiled CLI in ./dist.
5
-
6
- try {
7
- await import('./dist/index.js')
8
- } catch (err) {
9
- console.error('Build output not found. Please run:')
10
- console.error(' npm run build')
11
- console.error('or just:')
12
- console.error(' npm start')
13
- console.error('\nOriginal error:')
14
- console.error(err)
15
- process.exit(1)
16
- }