@muyichengshayu/promptx 0.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/README.md +80 -0
- package/apps/server/src/appPaths.js +102 -0
- package/apps/server/src/codex.js +585 -0
- package/apps/server/src/codexRunRuntime.js +212 -0
- package/apps/server/src/codexRuns.js +525 -0
- package/apps/server/src/codexSessions.js +149 -0
- package/apps/server/src/db.js +389 -0
- package/apps/server/src/gitDiff.js +1425 -0
- package/apps/server/src/index.js +909 -0
- package/apps/server/src/pdf.js +383 -0
- package/apps/server/src/repository.js +484 -0
- package/apps/server/src/sseHub.js +69 -0
- package/apps/server/src/upload.js +22 -0
- package/apps/server/src/workspaceFiles.js +662 -0
- package/apps/web/dist/assets/CodexSessionManagerDialog-c35LrKjV.js +6 -0
- package/apps/web/dist/assets/TaskDiffReviewDialog-BYcla0q4.js +12 -0
- package/apps/web/dist/assets/WorkbenchSettingsDialog-C0uQRStP.js +1 -0
- package/apps/web/dist/assets/WorkbenchView-Cp_qHNdX.js +216 -0
- package/apps/web/dist/assets/index-D9ui_gwj.js +25 -0
- package/apps/web/dist/assets/index-DDNrspNi.css +1 -0
- package/apps/web/dist/index.html +16 -0
- package/bin/promptx.js +60 -0
- package/package.json +58 -0
- package/packages/shared/src/index.js +121 -0
- package/scripts/doctor.mjs +251 -0
- package/scripts/service.mjs +308 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
6
|
+
'.git',
|
|
7
|
+
'.hg',
|
|
8
|
+
'.svn',
|
|
9
|
+
'.next',
|
|
10
|
+
'.nuxt',
|
|
11
|
+
'.output',
|
|
12
|
+
'.turbo',
|
|
13
|
+
'.cache',
|
|
14
|
+
'node_modules',
|
|
15
|
+
'dist',
|
|
16
|
+
'build',
|
|
17
|
+
'coverage',
|
|
18
|
+
'tmp',
|
|
19
|
+
'temp',
|
|
20
|
+
'uploads',
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
const DIRECTORY_PICKER_HIDDEN_NAMES = new Set([
|
|
24
|
+
'Applications',
|
|
25
|
+
'Downloads',
|
|
26
|
+
'Library',
|
|
27
|
+
'Movies',
|
|
28
|
+
'Music',
|
|
29
|
+
'Pictures',
|
|
30
|
+
'Public',
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
const DEFAULT_TREE_LIMIT = 200
|
|
34
|
+
const DEFAULT_SEARCH_LIMIT = 80
|
|
35
|
+
const MAX_SEARCH_VISITS = 20000
|
|
36
|
+
const DIRECTORY_PICKER_LIMIT = 240
|
|
37
|
+
|
|
38
|
+
function createHttpError(message, statusCode = 400) {
|
|
39
|
+
const error = new Error(message)
|
|
40
|
+
error.statusCode = statusCode
|
|
41
|
+
return error
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function toPosixPath(value = '') {
|
|
45
|
+
return String(value || '').replace(/\\/g, '/')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeRelativePath(relativePath = '') {
|
|
49
|
+
const value = toPosixPath(relativePath).trim()
|
|
50
|
+
if (!value || value === '.') {
|
|
51
|
+
return ''
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (value.includes('\0')) {
|
|
55
|
+
throw createHttpError('路径不合法。')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const cleaned = value
|
|
59
|
+
.replace(/^\/+/, '')
|
|
60
|
+
.replace(/^\.\/+/, '')
|
|
61
|
+
.replace(/\/{2,}/g, '/')
|
|
62
|
+
.replace(/\/+$/, '')
|
|
63
|
+
|
|
64
|
+
if (!cleaned) {
|
|
65
|
+
return ''
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const segments = cleaned.split('/')
|
|
69
|
+
if (segments.some((segment) => !segment || segment === '.' || segment === '..')) {
|
|
70
|
+
throw createHttpError('路径不合法。')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return cleaned
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ensurePathInsideWorkspace(workspacePath, targetPath) {
|
|
77
|
+
const root = path.resolve(String(workspacePath || ''))
|
|
78
|
+
const target = path.resolve(String(targetPath || ''))
|
|
79
|
+
|
|
80
|
+
if (target === root) {
|
|
81
|
+
return target
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (target.startsWith(`${root}${path.sep}`)) {
|
|
85
|
+
return target
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
throw createHttpError('只能访问当前工作目录内的文件。', 403)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveWorkspaceTarget(workspacePath, relativePath = '') {
|
|
92
|
+
const root = path.resolve(String(workspacePath || ''))
|
|
93
|
+
const normalizedRelativePath = normalizeRelativePath(relativePath)
|
|
94
|
+
const targetPath = normalizedRelativePath
|
|
95
|
+
? path.resolve(root, normalizedRelativePath)
|
|
96
|
+
: root
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
root,
|
|
100
|
+
relativePath: normalizedRelativePath,
|
|
101
|
+
absolutePath: ensurePathInsideWorkspace(root, targetPath),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getPathType(absolutePath = '') {
|
|
106
|
+
try {
|
|
107
|
+
const stats = fs.statSync(absolutePath)
|
|
108
|
+
if (stats.isDirectory()) {
|
|
109
|
+
return 'directory'
|
|
110
|
+
}
|
|
111
|
+
if (stats.isFile()) {
|
|
112
|
+
return 'file'
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
return ''
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return ''
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function shouldIgnoreDirectory(entry) {
|
|
122
|
+
return entry?.isDirectory?.() && IGNORED_DIRECTORY_NAMES.has(entry.name)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function shouldIgnorePickerDirectory(entry) {
|
|
126
|
+
if (!entry?.isDirectory?.()) {
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const name = String(entry.name || '').trim()
|
|
131
|
+
if (!name) {
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return name.startsWith('.')
|
|
136
|
+
|| IGNORED_DIRECTORY_NAMES.has(name)
|
|
137
|
+
|| DIRECTORY_PICKER_HIDDEN_NAMES.has(name)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function compareWorkspaceEntries(left, right) {
|
|
141
|
+
const typeDiff = Number(left.type !== 'directory') - Number(right.type !== 'directory')
|
|
142
|
+
if (typeDiff) {
|
|
143
|
+
return typeDiff
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return String(left.name || '').localeCompare(String(right.name || ''), 'zh-CN')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function compareDirectoryEntries(left, right) {
|
|
150
|
+
return String(left.name || left.path || '').localeCompare(String(right.name || right.path || ''), 'zh-CN')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildWorkspaceItem(workspacePath, absolutePath, entry, typeOverride = '') {
|
|
154
|
+
const type = typeOverride || (entry?.isDirectory?.() ? 'directory' : 'file')
|
|
155
|
+
const relativePath = path.relative(workspacePath, absolutePath)
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
name: entry?.name || path.basename(absolutePath),
|
|
159
|
+
path: toPosixPath(relativePath),
|
|
160
|
+
type,
|
|
161
|
+
hasChildren: type === 'directory' ? directoryHasVisibleChildren(absolutePath) : false,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function directoryHasVisibleChildren(directoryPath = '') {
|
|
166
|
+
try {
|
|
167
|
+
const entries = fs.readdirSync(directoryPath, { withFileTypes: true })
|
|
168
|
+
return entries.some((entry) => !shouldIgnoreDirectory(entry))
|
|
169
|
+
} catch {
|
|
170
|
+
return false
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function directoryHasVisiblePickerChildren(directoryPath = '') {
|
|
175
|
+
try {
|
|
176
|
+
const entries = fs.readdirSync(directoryPath, { withFileTypes: true })
|
|
177
|
+
return entries.some((entry) => entry.isDirectory() && !shouldIgnorePickerDirectory(entry))
|
|
178
|
+
} catch {
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function createDirectoryPickerItem(directoryPath = '', entryName = '') {
|
|
184
|
+
const normalizedPath = path.resolve(String(directoryPath || ''))
|
|
185
|
+
const parsed = path.parse(normalizedPath)
|
|
186
|
+
const isRoot = normalizedPath === parsed.root
|
|
187
|
+
const displayPath = normalizedPath
|
|
188
|
+
const displayName = entryName
|
|
189
|
+
|| (isRoot
|
|
190
|
+
? (process.platform === 'win32'
|
|
191
|
+
? normalizedPath.replace(/[\\/]+$/, '')
|
|
192
|
+
: normalizedPath)
|
|
193
|
+
: path.basename(normalizedPath))
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
name: displayName || displayPath,
|
|
197
|
+
path: displayPath,
|
|
198
|
+
type: 'directory',
|
|
199
|
+
hasChildren: directoryHasVisiblePickerChildren(normalizedPath),
|
|
200
|
+
isRoot,
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function getDirectoryPickerHomePath() {
|
|
205
|
+
return path.resolve(os.homedir())
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function normalizeDirectoryPickerPath(input = '') {
|
|
209
|
+
const value = String(input || '').trim()
|
|
210
|
+
if (!value) {
|
|
211
|
+
return ''
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const resolved = path.resolve(value)
|
|
215
|
+
if (!fs.existsSync(resolved)) {
|
|
216
|
+
throw createHttpError('目录不存在,请重新选择。', 404)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const stats = fs.statSync(resolved)
|
|
220
|
+
if (!stats.isDirectory()) {
|
|
221
|
+
throw createHttpError('只能选择文件夹。')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return resolved
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function getDirectoryParentPath(directoryPath = '') {
|
|
228
|
+
const resolved = path.resolve(String(directoryPath || ''))
|
|
229
|
+
const parsed = path.parse(resolved)
|
|
230
|
+
|
|
231
|
+
if (resolved === parsed.root) {
|
|
232
|
+
return ''
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const parentPath = path.dirname(resolved)
|
|
236
|
+
return parentPath === resolved ? '' : parentPath
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function clampLimit(value, fallback, max) {
|
|
240
|
+
const normalized = Number(value)
|
|
241
|
+
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
242
|
+
return fallback
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return Math.min(Math.floor(normalized), max)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function removeExtension(value = '') {
|
|
249
|
+
const normalized = String(value || '')
|
|
250
|
+
const extensionIndex = normalized.lastIndexOf('.')
|
|
251
|
+
if (extensionIndex <= 0) {
|
|
252
|
+
return normalized
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return normalized.slice(0, extensionIndex)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function splitSegmentWords(segment = '') {
|
|
259
|
+
return removeExtension(String(segment || ''))
|
|
260
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
261
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
262
|
+
.map((part) => part.trim().toLowerCase())
|
|
263
|
+
.filter(Boolean)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function scoreExactPrefixSubstring(candidate = '', query = '', weights = {}) {
|
|
267
|
+
const source = String(candidate || '').toLowerCase()
|
|
268
|
+
const keyword = String(query || '').trim().toLowerCase()
|
|
269
|
+
|
|
270
|
+
if (!source || !keyword) {
|
|
271
|
+
return 0
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (source === keyword) {
|
|
275
|
+
return weights.exact ?? 0
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (source.startsWith(keyword)) {
|
|
279
|
+
return (weights.prefix ?? 0) - source.length
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const substringIndex = source.indexOf(keyword)
|
|
283
|
+
if (substringIndex >= 0) {
|
|
284
|
+
return (weights.substring ?? 0) - substringIndex * 12 - Math.max(source.length - keyword.length, 0)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return 0
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function scoreCompactSubsequence(candidate = '', query = '') {
|
|
291
|
+
const source = String(candidate || '').toLowerCase()
|
|
292
|
+
const keyword = String(query || '').trim().toLowerCase()
|
|
293
|
+
|
|
294
|
+
if (!source || !keyword || keyword.length < 2 || keyword.length > source.length) {
|
|
295
|
+
return 0
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const positions = []
|
|
299
|
+
let searchIndex = 0
|
|
300
|
+
|
|
301
|
+
for (const char of keyword) {
|
|
302
|
+
const matchIndex = source.indexOf(char, searchIndex)
|
|
303
|
+
if (matchIndex < 0) {
|
|
304
|
+
return 0
|
|
305
|
+
}
|
|
306
|
+
positions.push(matchIndex)
|
|
307
|
+
searchIndex = matchIndex + 1
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const span = positions[positions.length - 1] - positions[0] + 1
|
|
311
|
+
const gap = span - keyword.length
|
|
312
|
+
const maxGap = Math.max(2, Math.floor(keyword.length * 0.6))
|
|
313
|
+
const coverage = keyword.length / span
|
|
314
|
+
|
|
315
|
+
if (gap > maxGap || coverage < 0.72) {
|
|
316
|
+
return 0
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return 3200 - gap * 120 - Math.max(source.length - keyword.length, 0)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function scorePathMatch(relativePath = '', query = '') {
|
|
323
|
+
const normalizedPath = toPosixPath(relativePath).toLowerCase()
|
|
324
|
+
const keyword = String(query || '').trim().toLowerCase()
|
|
325
|
+
|
|
326
|
+
if (!normalizedPath || !keyword) {
|
|
327
|
+
return 0
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (normalizedPath === keyword) {
|
|
331
|
+
return 16000
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (normalizedPath.startsWith(keyword)) {
|
|
335
|
+
return 13200 - normalizedPath.length
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const boundaryPrefixIndex = normalizedPath.indexOf(`/${keyword}`)
|
|
339
|
+
if (boundaryPrefixIndex >= 0) {
|
|
340
|
+
return 12000 - boundaryPrefixIndex * 8 - normalizedPath.length
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const substringIndex = normalizedPath.indexOf(keyword)
|
|
344
|
+
if (substringIndex >= 0) {
|
|
345
|
+
return 9800 - substringIndex * 12 - Math.max(normalizedPath.length - keyword.length, 0)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return 0
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function scoreSegmentMatch(segment = '', query = '', options = {}) {
|
|
352
|
+
const keyword = String(query || '').trim().toLowerCase()
|
|
353
|
+
const normalizedSegment = String(segment || '').toLowerCase()
|
|
354
|
+
const bareSegment = removeExtension(normalizedSegment)
|
|
355
|
+
|
|
356
|
+
if (!normalizedSegment || !keyword) {
|
|
357
|
+
return 0
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const exactWeights = options.isFileName
|
|
361
|
+
? { exact: 15000, prefix: 12600, substring: 10600 }
|
|
362
|
+
: { exact: 12400, prefix: 10400, substring: 9000 }
|
|
363
|
+
|
|
364
|
+
let bestScore = Math.max(
|
|
365
|
+
scoreExactPrefixSubstring(normalizedSegment, keyword, exactWeights),
|
|
366
|
+
scoreExactPrefixSubstring(bareSegment, keyword, exactWeights)
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
const words = splitSegmentWords(segment)
|
|
370
|
+
if (words.length) {
|
|
371
|
+
const initials = words.map((word) => word[0]).join('')
|
|
372
|
+
const compact = words.join('')
|
|
373
|
+
|
|
374
|
+
for (const word of words) {
|
|
375
|
+
bestScore = Math.max(
|
|
376
|
+
bestScore,
|
|
377
|
+
scoreExactPrefixSubstring(word, keyword, {
|
|
378
|
+
exact: 11600,
|
|
379
|
+
prefix: 9800,
|
|
380
|
+
substring: 8200,
|
|
381
|
+
})
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (initials && keyword.length >= 2 && initials.startsWith(keyword)) {
|
|
386
|
+
bestScore = Math.max(bestScore, 6000 - initials.length * 10)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
bestScore = Math.max(bestScore, scoreCompactSubsequence(compact || bareSegment, keyword))
|
|
390
|
+
} else {
|
|
391
|
+
bestScore = Math.max(bestScore, scoreCompactSubsequence(bareSegment || normalizedSegment, keyword))
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return bestScore
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function scoreWorkspaceMatch(relativePath = '', query = '') {
|
|
398
|
+
const normalizedPath = toPosixPath(relativePath)
|
|
399
|
+
const keyword = String(query || '').trim().toLowerCase()
|
|
400
|
+
|
|
401
|
+
if (!normalizedPath || !keyword) {
|
|
402
|
+
return 0
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const pathScore = scorePathMatch(normalizedPath, keyword)
|
|
406
|
+
if (keyword.includes('/')) {
|
|
407
|
+
return pathScore
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const segments = normalizedPath.split('/').filter(Boolean)
|
|
411
|
+
const fileName = segments.at(-1) || normalizedPath
|
|
412
|
+
const nameScore = scoreSegmentMatch(fileName, keyword, { isFileName: true })
|
|
413
|
+
const segmentScore = segments.reduce((bestScore, segment, index) => Math.max(
|
|
414
|
+
bestScore,
|
|
415
|
+
scoreSegmentMatch(segment, keyword, { isFileName: index === segments.length - 1 })
|
|
416
|
+
), 0)
|
|
417
|
+
|
|
418
|
+
return Math.max(pathScore, nameScore, segmentScore)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function listWorkspaceTree(workspacePath, options = {}) {
|
|
422
|
+
const target = resolveWorkspaceTarget(workspacePath, options.path)
|
|
423
|
+
const type = getPathType(target.absolutePath)
|
|
424
|
+
|
|
425
|
+
if (!type) {
|
|
426
|
+
throw createHttpError('目标路径不存在。', 404)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (type !== 'directory') {
|
|
430
|
+
throw createHttpError('只能展开目录。')
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const limit = clampLimit(options.limit, DEFAULT_TREE_LIMIT, 500)
|
|
434
|
+
const entries = fs.readdirSync(target.absolutePath, { withFileTypes: true })
|
|
435
|
+
.filter((entry) => !shouldIgnoreDirectory(entry))
|
|
436
|
+
.map((entry) => buildWorkspaceItem(target.root, path.join(target.absolutePath, entry.name), entry))
|
|
437
|
+
.sort(compareWorkspaceEntries)
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
cwd: target.root,
|
|
441
|
+
path: target.relativePath,
|
|
442
|
+
parentPath: target.relativePath.includes('/')
|
|
443
|
+
? target.relativePath.slice(0, target.relativePath.lastIndexOf('/'))
|
|
444
|
+
: '',
|
|
445
|
+
items: entries.slice(0, limit),
|
|
446
|
+
truncated: entries.length > limit,
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function searchWorkspaceEntries(workspacePath, options = {}) {
|
|
451
|
+
const root = path.resolve(String(workspacePath || ''))
|
|
452
|
+
const query = String(options.query || '').trim()
|
|
453
|
+
const limit = clampLimit(options.limit, DEFAULT_SEARCH_LIMIT, 200)
|
|
454
|
+
|
|
455
|
+
if (!query) {
|
|
456
|
+
return {
|
|
457
|
+
cwd: root,
|
|
458
|
+
query: '',
|
|
459
|
+
items: [],
|
|
460
|
+
truncated: false,
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const matches = []
|
|
465
|
+
let visited = 0
|
|
466
|
+
let truncated = false
|
|
467
|
+
const stack = ['']
|
|
468
|
+
|
|
469
|
+
while (stack.length) {
|
|
470
|
+
const currentRelativePath = stack.pop()
|
|
471
|
+
const currentAbsolutePath = currentRelativePath
|
|
472
|
+
? path.join(root, currentRelativePath)
|
|
473
|
+
: root
|
|
474
|
+
|
|
475
|
+
let entries = []
|
|
476
|
+
try {
|
|
477
|
+
entries = fs.readdirSync(currentAbsolutePath, { withFileTypes: true })
|
|
478
|
+
} catch {
|
|
479
|
+
continue
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
entries.sort((left, right) => left.name.localeCompare(right.name, 'zh-CN'))
|
|
483
|
+
|
|
484
|
+
for (const entry of entries) {
|
|
485
|
+
if (shouldIgnoreDirectory(entry)) {
|
|
486
|
+
continue
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
visited += 1
|
|
490
|
+
if (visited > MAX_SEARCH_VISITS) {
|
|
491
|
+
truncated = true
|
|
492
|
+
stack.length = 0
|
|
493
|
+
break
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const relativePath = currentRelativePath
|
|
497
|
+
? `${toPosixPath(currentRelativePath)}/${entry.name}`
|
|
498
|
+
: entry.name
|
|
499
|
+
const absolutePath = path.join(currentAbsolutePath, entry.name)
|
|
500
|
+
const type = entry.isDirectory() ? 'directory' : 'file'
|
|
501
|
+
const score = scoreWorkspaceMatch(relativePath, query)
|
|
502
|
+
|
|
503
|
+
if (score > 0) {
|
|
504
|
+
matches.push({
|
|
505
|
+
...buildWorkspaceItem(root, absolutePath, entry, type),
|
|
506
|
+
score,
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (entry.isDirectory()) {
|
|
511
|
+
stack.push(path.join(currentRelativePath, entry.name))
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
matches.sort((left, right) => {
|
|
517
|
+
const scoreDiff = right.score - left.score
|
|
518
|
+
if (scoreDiff) {
|
|
519
|
+
return scoreDiff
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const typeDiff = Number(left.type !== 'directory') - Number(right.type !== 'directory')
|
|
523
|
+
if (typeDiff) {
|
|
524
|
+
return typeDiff
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const pathLengthDiff = left.path.length - right.path.length
|
|
528
|
+
if (pathLengthDiff) {
|
|
529
|
+
return pathLengthDiff
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return left.path.localeCompare(right.path, 'zh-CN')
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
cwd: root,
|
|
537
|
+
query,
|
|
538
|
+
items: matches.slice(0, limit).map(({ score, ...item }) => item),
|
|
539
|
+
truncated: truncated || matches.length > limit,
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function listDirectoryPickerTree(options = {}) {
|
|
544
|
+
const targetPath = normalizeDirectoryPickerPath(options.path) || getDirectoryPickerHomePath()
|
|
545
|
+
|
|
546
|
+
const limit = clampLimit(options.limit, DIRECTORY_PICKER_LIMIT, 600)
|
|
547
|
+
const entries = fs.readdirSync(targetPath, { withFileTypes: true })
|
|
548
|
+
.filter((entry) => entry.isDirectory() && !shouldIgnorePickerDirectory(entry))
|
|
549
|
+
.map((entry) => createDirectoryPickerItem(path.join(targetPath, entry.name), entry.name))
|
|
550
|
+
.sort(compareDirectoryEntries)
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
path: targetPath,
|
|
554
|
+
parentPath: '',
|
|
555
|
+
items: entries.slice(0, limit),
|
|
556
|
+
truncated: entries.length > limit,
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function scoreDirectoryPickerMatch(directoryPath = '', basePath = '', query = '') {
|
|
561
|
+
const normalizedQuery = String(query || '').trim().toLowerCase()
|
|
562
|
+
if (!directoryPath || !normalizedQuery) {
|
|
563
|
+
return 0
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const absoluteScore = scoreWorkspaceMatch(toPosixPath(directoryPath), normalizedQuery)
|
|
567
|
+
const base = String(basePath || '').trim()
|
|
568
|
+
const relativePath = base ? toPosixPath(path.relative(base, directoryPath)) : toPosixPath(directoryPath)
|
|
569
|
+
const relativeScore = scoreWorkspaceMatch(relativePath, normalizedQuery)
|
|
570
|
+
const nameScore = scoreSegmentMatch(path.basename(directoryPath), normalizedQuery, { isFileName: false })
|
|
571
|
+
return Math.max(absoluteScore, relativeScore, nameScore)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function searchDirectoryPickerEntries(options = {}) {
|
|
575
|
+
const query = String(options.query || '').trim()
|
|
576
|
+
const limit = clampLimit(options.limit, DEFAULT_SEARCH_LIMIT, 200)
|
|
577
|
+
|
|
578
|
+
if (!query) {
|
|
579
|
+
return {
|
|
580
|
+
path: '',
|
|
581
|
+
query: '',
|
|
582
|
+
items: [],
|
|
583
|
+
truncated: false,
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const targetPath = normalizeDirectoryPickerPath(options.path) || getDirectoryPickerHomePath()
|
|
588
|
+
const roots = [targetPath]
|
|
589
|
+
const matches = []
|
|
590
|
+
let visited = 0
|
|
591
|
+
let truncated = false
|
|
592
|
+
|
|
593
|
+
for (const rootPath of roots) {
|
|
594
|
+
const stack = [rootPath]
|
|
595
|
+
|
|
596
|
+
while (stack.length) {
|
|
597
|
+
const currentPath = stack.pop()
|
|
598
|
+
let entries = []
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
entries = fs.readdirSync(currentPath, { withFileTypes: true })
|
|
602
|
+
} catch {
|
|
603
|
+
continue
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
entries.sort((left, right) => left.name.localeCompare(right.name, 'zh-CN'))
|
|
607
|
+
|
|
608
|
+
for (const entry of entries) {
|
|
609
|
+
if (!entry.isDirectory() || shouldIgnorePickerDirectory(entry)) {
|
|
610
|
+
continue
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
visited += 1
|
|
614
|
+
if (visited > MAX_SEARCH_VISITS) {
|
|
615
|
+
truncated = true
|
|
616
|
+
stack.length = 0
|
|
617
|
+
break
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const absolutePath = path.join(currentPath, entry.name)
|
|
621
|
+
const score = scoreDirectoryPickerMatch(absolutePath, rootPath, query)
|
|
622
|
+
if (score > 0) {
|
|
623
|
+
matches.push({
|
|
624
|
+
...createDirectoryPickerItem(absolutePath, entry.name),
|
|
625
|
+
score,
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
stack.push(absolutePath)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (truncated) {
|
|
633
|
+
break
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (truncated) {
|
|
638
|
+
break
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
matches.sort((left, right) => {
|
|
643
|
+
const scoreDiff = right.score - left.score
|
|
644
|
+
if (scoreDiff) {
|
|
645
|
+
return scoreDiff
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const depthDiff = left.path.split(path.sep).length - right.path.split(path.sep).length
|
|
649
|
+
if (depthDiff) {
|
|
650
|
+
return depthDiff
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return String(left.path || '').localeCompare(String(right.path || ''), 'zh-CN')
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
return {
|
|
657
|
+
path: targetPath,
|
|
658
|
+
query,
|
|
659
|
+
items: matches.slice(0, limit).map(({ score, ...item }) => item),
|
|
660
|
+
truncated: truncated || matches.length > limit,
|
|
661
|
+
}
|
|
662
|
+
}
|