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.
- package/.github/workflows/publish.yml +41 -0
- package/README.md +120 -0
- package/dist/index.js +76 -18
- package/dist/index.js.map +1 -1
- package/dist/lib/data-loader.js +289 -56
- package/dist/lib/data-loader.js.map +1 -1
- package/dist/lib/paths.js +31 -7
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/workspace-resolver.js +211 -0
- package/dist/lib/workspace-resolver.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +86 -20
- package/src/lib/data-loader.ts +346 -67
- package/src/lib/paths.ts +33 -8
- package/src/lib/workspace-resolver.ts +235 -0
- package/index.js +0 -16
|
@@ -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
|
-
}
|