@tnotesjs/core 0.2.1 → 0.2.2
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/config/ConfigManager.ts +137 -0
- package/config/constants.ts +121 -0
- package/config/index.ts +25 -0
- package/config/templates.ts +49 -0
- package/core/GitManager.ts +513 -0
- package/core/NoteIndexCache.ts +194 -0
- package/core/NoteManager.ts +407 -0
- package/core/ProcessManager.ts +180 -0
- package/core/ReadmeGenerator.ts +215 -0
- package/core/TocGenerator.ts +212 -0
- package/core/index.ts +11 -0
- package/package.json +5 -2
- package/services/file-watcher/configChangeHandler.ts +64 -0
- package/services/file-watcher/eventScheduler.ts +179 -0
- package/services/file-watcher/folderChangeHandler.ts +325 -0
- package/services/file-watcher/fsWatcherAdapter.ts +128 -0
- package/services/file-watcher/globalUpdateCoordinator.ts +60 -0
- package/services/file-watcher/index.ts +7 -0
- package/services/file-watcher/internal.ts +79 -0
- package/services/file-watcher/readmeChangeHandler.ts +28 -0
- package/services/file-watcher/renameDetector.ts +120 -0
- package/services/file-watcher/service.ts +352 -0
- package/services/file-watcher/watchState.ts +194 -0
- package/services/git/index.ts +7 -0
- package/services/git/service.ts +114 -0
- package/services/index.ts +15 -0
- package/services/init-sub-repo/index.ts +2 -0
- package/services/init-sub-repo/initSubRepoLogic.test.ts +162 -0
- package/services/init-sub-repo/initSubRepoLogic.ts +304 -0
- package/services/init-sub-repo/service.ts +101 -0
- package/services/note/index.ts +7 -0
- package/services/note/service.ts +362 -0
- package/services/readme/index.ts +7 -0
- package/services/readme/service.ts +761 -0
- package/services/timestamp/index.ts +7 -0
- package/services/timestamp/service.ts +465 -0
- package/services/toc/index.ts +5 -0
- package/services/toc/moveTocInside.test.ts +73 -0
- package/services/toc/service.ts +759 -0
- package/services/vitepress/index.ts +7 -0
- package/services/vitepress/service.ts +339 -0
- package/utils/errorHandler.ts +174 -0
- package/utils/file.ts +17 -0
- package/utils/genHierarchicalSidebar.ts +69 -0
- package/utils/generateAnchor.ts +24 -0
- package/utils/getChangedIds.ts +35 -0
- package/utils/index.ts +71 -0
- package/utils/logger.ts +231 -0
- package/utils/markdown.ts +75 -0
- package/utils/migrateReadmeToToc.test.ts +111 -0
- package/utils/migrateReadmeToToc.ts +135 -0
- package/utils/parseArgs.ts +90 -0
- package/utils/parseReadmeCompletedNotes.test.ts +90 -0
- package/utils/parseReadmeCompletedNotes.ts +108 -0
- package/utils/portUtils.ts +113 -0
- package/utils/readmeHelpers.ts +190 -0
- package/utils/runCommand.ts +29 -0
- package/utils/tocHelpers.test.ts +278 -0
- package/utils/tocHelpers.ts +855 -0
- package/utils/tocNodeId.test.ts +60 -0
- package/utils/tocNodeId.ts +97 -0
- package/utils/validators.ts +102 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/GitManager.ts
|
|
3
|
+
*
|
|
4
|
+
* Git 仓库管理器 - 提供统一的 Git 操作接口
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Logger, runCommand, createError, handleError } from '../utils'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Git 文件状态接口
|
|
11
|
+
*/
|
|
12
|
+
interface GitFileStatus {
|
|
13
|
+
path: string
|
|
14
|
+
status: 'staged' | 'unstaged' | 'untracked' | 'modified'
|
|
15
|
+
statusCode: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Git 状态信息接口
|
|
20
|
+
*/
|
|
21
|
+
interface GitStatus {
|
|
22
|
+
hasChanges: boolean
|
|
23
|
+
changedFiles: number
|
|
24
|
+
staged: number
|
|
25
|
+
unstaged: number
|
|
26
|
+
untracked: number
|
|
27
|
+
branch: string
|
|
28
|
+
ahead: number
|
|
29
|
+
behind: number
|
|
30
|
+
files: GitFileStatus[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Git 远程信息接口
|
|
35
|
+
*/
|
|
36
|
+
interface GitRemoteInfo {
|
|
37
|
+
url: string
|
|
38
|
+
type: 'https' | 'ssh' | 'unknown'
|
|
39
|
+
owner?: string
|
|
40
|
+
repo?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Git 管理器类
|
|
45
|
+
*/
|
|
46
|
+
export class GitManager {
|
|
47
|
+
private logger: Logger
|
|
48
|
+
private dir: string
|
|
49
|
+
|
|
50
|
+
constructor(dir: string, logger?: Logger) {
|
|
51
|
+
this.dir = dir
|
|
52
|
+
this.logger = logger?.child('git') || new Logger({ prefix: 'git' })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 检查是否为有效的 Git 仓库
|
|
57
|
+
*/
|
|
58
|
+
async isValidRepo(): Promise<boolean> {
|
|
59
|
+
try {
|
|
60
|
+
const result = await runCommand(
|
|
61
|
+
'git rev-parse --is-inside-work-tree',
|
|
62
|
+
this.dir,
|
|
63
|
+
)
|
|
64
|
+
return result.trim() === 'true'
|
|
65
|
+
} catch {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 确保是有效的 Git 仓库,否则抛出错误
|
|
72
|
+
*/
|
|
73
|
+
async ensureValidRepo(): Promise<void> {
|
|
74
|
+
if (!(await this.isValidRepo())) {
|
|
75
|
+
throw createError.gitNotRepo(this.dir)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 获取 Git 状态
|
|
81
|
+
*/
|
|
82
|
+
async getStatus(): Promise<GitStatus> {
|
|
83
|
+
await this.ensureValidRepo()
|
|
84
|
+
|
|
85
|
+
// 使用 -c core.quotePath=false 禁用路径转义,正确显示中文文件名
|
|
86
|
+
const statusOutput = await runCommand(
|
|
87
|
+
'git -c core.quotePath=false status --porcelain',
|
|
88
|
+
this.dir,
|
|
89
|
+
)
|
|
90
|
+
const lines = statusOutput
|
|
91
|
+
.trim()
|
|
92
|
+
.split('\n')
|
|
93
|
+
.filter((line) => line)
|
|
94
|
+
|
|
95
|
+
// 解析文件状态
|
|
96
|
+
const files: GitFileStatus[] = lines.map((line) => {
|
|
97
|
+
const statusCode = line.substring(0, 2)
|
|
98
|
+
let path = line.substring(3)
|
|
99
|
+
|
|
100
|
+
// 移除 git 添加的引号(即使设置了 core.quotePath=false,某些情况下仍会加引号)
|
|
101
|
+
path = path.replace(/^"(.*)"$/, '$1')
|
|
102
|
+
|
|
103
|
+
let status: GitFileStatus['status'] = 'modified'
|
|
104
|
+
if (line.startsWith('??')) {
|
|
105
|
+
status = 'untracked'
|
|
106
|
+
} else if (/^[MADRC]/.test(statusCode)) {
|
|
107
|
+
status = 'staged'
|
|
108
|
+
} else if (/^.[MD]/.test(statusCode)) {
|
|
109
|
+
status = 'unstaged'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { path, status, statusCode }
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const staged = files.filter((f) => f.status === 'staged').length
|
|
116
|
+
const unstaged = files.filter((f) => f.status === 'unstaged').length
|
|
117
|
+
const untracked = files.filter((f) => f.status === 'untracked').length
|
|
118
|
+
|
|
119
|
+
// 获取当前分支
|
|
120
|
+
const branch = await runCommand('git branch --show-current', this.dir)
|
|
121
|
+
|
|
122
|
+
// 获取远程同步状态
|
|
123
|
+
let ahead = 0
|
|
124
|
+
let behind = 0
|
|
125
|
+
try {
|
|
126
|
+
const aheadBehind = await runCommand(
|
|
127
|
+
'git rev-list --left-right --count @{upstream}...HEAD',
|
|
128
|
+
this.dir,
|
|
129
|
+
)
|
|
130
|
+
const [behindStr, aheadStr] = aheadBehind.trim().split('\t')
|
|
131
|
+
behind = parseInt(behindStr) || 0
|
|
132
|
+
ahead = parseInt(aheadStr) || 0
|
|
133
|
+
} catch {
|
|
134
|
+
// 可能没有上游分支
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
hasChanges: lines.length > 0,
|
|
139
|
+
changedFiles: lines.length,
|
|
140
|
+
staged,
|
|
141
|
+
unstaged,
|
|
142
|
+
untracked,
|
|
143
|
+
branch: branch.trim(),
|
|
144
|
+
ahead,
|
|
145
|
+
behind,
|
|
146
|
+
files,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 获取远程仓库信息
|
|
152
|
+
*/
|
|
153
|
+
async getRemoteInfo(): Promise<GitRemoteInfo | null> {
|
|
154
|
+
try {
|
|
155
|
+
await this.ensureValidRepo()
|
|
156
|
+
const remoteUrl = await runCommand(
|
|
157
|
+
'git config --get remote.origin.url',
|
|
158
|
+
this.dir,
|
|
159
|
+
)
|
|
160
|
+
const url = remoteUrl.trim()
|
|
161
|
+
|
|
162
|
+
if (!url) return null
|
|
163
|
+
|
|
164
|
+
// 解析 HTTPS URL
|
|
165
|
+
const httpsMatch = url.match(
|
|
166
|
+
/https:\/\/(?:www\.)?github\.com\/([^/]+)\/(.+?)(?:\.git)?$/,
|
|
167
|
+
)
|
|
168
|
+
if (httpsMatch) {
|
|
169
|
+
return {
|
|
170
|
+
url,
|
|
171
|
+
type: 'https',
|
|
172
|
+
owner: httpsMatch[1],
|
|
173
|
+
repo: httpsMatch[2],
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 解析 SSH URL
|
|
178
|
+
const sshMatch = url.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/)
|
|
179
|
+
if (sshMatch) {
|
|
180
|
+
return {
|
|
181
|
+
url,
|
|
182
|
+
type: 'ssh',
|
|
183
|
+
owner: sshMatch[1],
|
|
184
|
+
repo: sshMatch[2],
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { url, type: 'unknown' }
|
|
189
|
+
} catch {
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 检查是否有未提交的更改
|
|
196
|
+
*/
|
|
197
|
+
async hasUncommittedChanges(): Promise<boolean> {
|
|
198
|
+
const status = await this.getStatus()
|
|
199
|
+
return status.hasChanges
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Stash 当前更改
|
|
204
|
+
*/
|
|
205
|
+
async stash(message?: string): Promise<boolean> {
|
|
206
|
+
try {
|
|
207
|
+
await this.ensureValidRepo()
|
|
208
|
+
const cmd = message ? `git stash push -m "${message}"` : 'git stash push'
|
|
209
|
+
await runCommand(cmd, this.dir)
|
|
210
|
+
this.logger.info('Stashed uncommitted changes')
|
|
211
|
+
return true
|
|
212
|
+
} catch (error) {
|
|
213
|
+
this.logger.warn('Failed to stash changes')
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Pop stash
|
|
220
|
+
*/
|
|
221
|
+
async stashPop(): Promise<boolean> {
|
|
222
|
+
try {
|
|
223
|
+
await this.ensureValidRepo()
|
|
224
|
+
await runCommand('git stash pop', this.dir)
|
|
225
|
+
this.logger.info('Restored stashed changes')
|
|
226
|
+
return true
|
|
227
|
+
} catch (error) {
|
|
228
|
+
this.logger.warn('Failed to restore stashed changes')
|
|
229
|
+
return false
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 拉取远程更新
|
|
235
|
+
*/
|
|
236
|
+
async pull(options?: {
|
|
237
|
+
rebase?: boolean
|
|
238
|
+
autostash?: boolean
|
|
239
|
+
}): Promise<void> {
|
|
240
|
+
await this.ensureValidRepo()
|
|
241
|
+
|
|
242
|
+
const { rebase = true, autostash = true } = options || {}
|
|
243
|
+
|
|
244
|
+
// 检查是否有未提交的更改
|
|
245
|
+
const hasChanges = await this.hasUncommittedChanges()
|
|
246
|
+
let didStash = false
|
|
247
|
+
|
|
248
|
+
if (hasChanges && !autostash) {
|
|
249
|
+
this.logger.warn('Repository has uncommitted changes')
|
|
250
|
+
didStash = await this.stash('Auto-stash before pull')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
// 获取远程更新前记录当前提交
|
|
255
|
+
const beforeCommit = await runCommand('git rev-parse HEAD', this.dir)
|
|
256
|
+
|
|
257
|
+
this.logger.info('正在拉取远程更新...')
|
|
258
|
+
|
|
259
|
+
const cmd = `git pull ${rebase ? '--rebase' : ''} ${
|
|
260
|
+
autostash ? '--autostash' : ''
|
|
261
|
+
}`.trim()
|
|
262
|
+
await runCommand(cmd, this.dir)
|
|
263
|
+
|
|
264
|
+
// 获取拉取后的提交
|
|
265
|
+
const afterCommit = await runCommand('git rev-parse HEAD', this.dir)
|
|
266
|
+
|
|
267
|
+
// 如果有更新,显示更新的文件列表
|
|
268
|
+
if (beforeCommit.trim() !== afterCommit.trim()) {
|
|
269
|
+
try {
|
|
270
|
+
const diffOutput = await runCommand(
|
|
271
|
+
`git diff --name-only ${beforeCommit.trim()}..${afterCommit.trim()}`,
|
|
272
|
+
this.dir,
|
|
273
|
+
)
|
|
274
|
+
const changedFiles = diffOutput
|
|
275
|
+
.trim()
|
|
276
|
+
.split('\n')
|
|
277
|
+
.filter((f) => f)
|
|
278
|
+
|
|
279
|
+
if (changedFiles.length > 0) {
|
|
280
|
+
console.log(` 更新了 ${changedFiles.length} 个文件:`)
|
|
281
|
+
changedFiles.forEach((file, index) => {
|
|
282
|
+
console.log(` ${index + 1}. ${file}`)
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
this.logger.success(`拉取成功: ${changedFiles.length} 个文件已更新`)
|
|
287
|
+
} catch {
|
|
288
|
+
this.logger.success('拉取成功')
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
this.logger.info('已是最新,没有需要拉取的更新')
|
|
292
|
+
}
|
|
293
|
+
} catch (error) {
|
|
294
|
+
this.logger.error('拉取失败')
|
|
295
|
+
handleError(error)
|
|
296
|
+
throw error
|
|
297
|
+
} finally {
|
|
298
|
+
// 如果之前手动 stash 了,尝试 pop
|
|
299
|
+
if (didStash) {
|
|
300
|
+
await this.stashPop()
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 提交更改
|
|
307
|
+
*/
|
|
308
|
+
async commit(message: string): Promise<void> {
|
|
309
|
+
await this.ensureValidRepo()
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
await runCommand(`git commit -m "${message}"`, this.dir)
|
|
313
|
+
this.logger.success(`Committed: ${message}`)
|
|
314
|
+
} catch (error) {
|
|
315
|
+
handleError(error)
|
|
316
|
+
throw error
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 添加文件到暂存区
|
|
322
|
+
*/
|
|
323
|
+
async add(files: string | string[] = '.'): Promise<void> {
|
|
324
|
+
await this.ensureValidRepo()
|
|
325
|
+
|
|
326
|
+
const fileList = Array.isArray(files) ? files.join(' ') : files
|
|
327
|
+
try {
|
|
328
|
+
await runCommand(`git add ${fileList}`, this.dir)
|
|
329
|
+
this.logger.info(`Staged changes: ${fileList}`)
|
|
330
|
+
} catch (error) {
|
|
331
|
+
handleError(error)
|
|
332
|
+
throw error
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 推送到远程仓库
|
|
338
|
+
*/
|
|
339
|
+
async push(options?: {
|
|
340
|
+
force?: boolean
|
|
341
|
+
setUpstream?: boolean
|
|
342
|
+
}): Promise<void> {
|
|
343
|
+
await this.ensureValidRepo()
|
|
344
|
+
|
|
345
|
+
const { force = false, setUpstream = false } = options || {}
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const status = await this.getStatus()
|
|
349
|
+
this.logger.progress(`正在推送到远程 (${status.branch})...`)
|
|
350
|
+
|
|
351
|
+
let cmd = 'git push'
|
|
352
|
+
if (force) cmd += ' --force'
|
|
353
|
+
if (setUpstream) cmd += ` --set-upstream origin ${status.branch}`
|
|
354
|
+
|
|
355
|
+
await runCommand(cmd, this.dir)
|
|
356
|
+
|
|
357
|
+
const remoteInfo = await this.getRemoteInfo()
|
|
358
|
+
if (remoteInfo) {
|
|
359
|
+
this.logger.success(`推送成功 → ${remoteInfo.owner}/${remoteInfo.repo}`)
|
|
360
|
+
} else {
|
|
361
|
+
this.logger.success('推送成功')
|
|
362
|
+
}
|
|
363
|
+
} catch (error) {
|
|
364
|
+
this.logger.error('推送失败')
|
|
365
|
+
handleError(error)
|
|
366
|
+
throw error
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 完整的推送流程:检查 -> 添加 -> 提交 -> 推送
|
|
372
|
+
*/
|
|
373
|
+
async pushWithCommit(
|
|
374
|
+
commitMessage?: string,
|
|
375
|
+
options?: { force?: boolean; showFiles?: boolean },
|
|
376
|
+
): Promise<void> {
|
|
377
|
+
await this.ensureValidRepo()
|
|
378
|
+
|
|
379
|
+
const status = await this.getStatus()
|
|
380
|
+
|
|
381
|
+
// 检查是否有更改
|
|
382
|
+
if (!status.hasChanges) {
|
|
383
|
+
this.logger.info('没有需要提交的更改')
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
// 显示开始信息和文件列表
|
|
389
|
+
this.logger.info(`正在推送 ${status.changedFiles} 个文件...`)
|
|
390
|
+
|
|
391
|
+
// 显示文件列表
|
|
392
|
+
status.files.forEach((file, index) => {
|
|
393
|
+
console.log(` ${index + 1}. ${file.path}`)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
// 添加所有更改(静默执行)
|
|
397
|
+
await runCommand('git add .', this.dir)
|
|
398
|
+
|
|
399
|
+
// 生成提交信息
|
|
400
|
+
const message =
|
|
401
|
+
commitMessage || `update: ${status.changedFiles} files modified`
|
|
402
|
+
|
|
403
|
+
// 提交(静默执行)
|
|
404
|
+
await runCommand(`git commit -m "${message}"`, this.dir)
|
|
405
|
+
|
|
406
|
+
// 推送(静默执行)
|
|
407
|
+
let cmd = 'git push'
|
|
408
|
+
if (options?.force) cmd += ' --force'
|
|
409
|
+
|
|
410
|
+
await runCommand(cmd, this.dir)
|
|
411
|
+
|
|
412
|
+
// 只在成功时显示结果
|
|
413
|
+
const remoteInfo = await this.getRemoteInfo()
|
|
414
|
+
if (remoteInfo) {
|
|
415
|
+
this.logger.success(
|
|
416
|
+
`推送成功: ${status.changedFiles} 个文件 → https://github.com/${remoteInfo.owner}/${remoteInfo.repo}`,
|
|
417
|
+
)
|
|
418
|
+
} else {
|
|
419
|
+
this.logger.success(`推送成功: ${status.changedFiles} 个文件`)
|
|
420
|
+
}
|
|
421
|
+
} catch (error) {
|
|
422
|
+
// 失败时显示完整错误信息
|
|
423
|
+
this.logger.error(`推送失败`)
|
|
424
|
+
handleError(error)
|
|
425
|
+
throw error
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* 完整的同步流程:拉取 -> 推送
|
|
431
|
+
*/
|
|
432
|
+
async sync(options?: {
|
|
433
|
+
commitMessage?: string
|
|
434
|
+
rebase?: boolean
|
|
435
|
+
}): Promise<void> {
|
|
436
|
+
const { commitMessage, rebase = true } = options || {}
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
// 先拉取
|
|
440
|
+
await this.pull({ rebase, autostash: true })
|
|
441
|
+
|
|
442
|
+
// 再推送
|
|
443
|
+
await this.pushWithCommit(commitMessage)
|
|
444
|
+
} catch (error) {
|
|
445
|
+
this.logger.error('Sync failed')
|
|
446
|
+
handleError(error)
|
|
447
|
+
throw error
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* 显示状态摘要
|
|
453
|
+
*/
|
|
454
|
+
async showStatus(options?: { showFiles?: boolean }): Promise<void> {
|
|
455
|
+
const { showFiles = true } = options || {}
|
|
456
|
+
const status = await this.getStatus()
|
|
457
|
+
const remoteInfo = await this.getRemoteInfo()
|
|
458
|
+
|
|
459
|
+
console.log('\n📊 Git 状态:')
|
|
460
|
+
console.log(` 分支: ${status.branch}`)
|
|
461
|
+
if (remoteInfo) {
|
|
462
|
+
console.log(
|
|
463
|
+
` 远程: ${remoteInfo.owner}/${remoteInfo.repo} (${remoteInfo.type})`,
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (status.hasChanges) {
|
|
468
|
+
console.log(
|
|
469
|
+
` 变更: ${status.changedFiles} 个文件 (已暂存 ${status.staged}, 未暂存 ${status.unstaged}, 未跟踪 ${status.untracked})`,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
// 显示文件列表
|
|
473
|
+
if (showFiles && status.files.length > 0) {
|
|
474
|
+
console.log(' 变更文件列表:')
|
|
475
|
+
|
|
476
|
+
// 按状态分组显示
|
|
477
|
+
const stagedFiles = status.files.filter((f) => f.status === 'staged')
|
|
478
|
+
const unstagedFiles = status.files.filter(
|
|
479
|
+
(f) => f.status === 'unstaged',
|
|
480
|
+
)
|
|
481
|
+
const untrackedFiles = status.files.filter(
|
|
482
|
+
(f) => f.status === 'untracked',
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if (stagedFiles.length > 0) {
|
|
486
|
+
console.log(' 已暂存:')
|
|
487
|
+
stagedFiles.forEach((f) => console.log(` ✓ ${f.path}`))
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (unstagedFiles.length > 0) {
|
|
491
|
+
console.log(' 未暂存:')
|
|
492
|
+
unstagedFiles.forEach((f) => console.log(` • ${f.path}`))
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (untrackedFiles.length > 0) {
|
|
496
|
+
console.log(' 未跟踪:')
|
|
497
|
+
untrackedFiles.forEach((f) => console.log(` ? ${f.path}`))
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
} else {
|
|
501
|
+
console.log(' 状态: 工作区干净,没有变更')
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (status.ahead > 0 || status.behind > 0) {
|
|
505
|
+
const syncInfo = []
|
|
506
|
+
if (status.ahead > 0) syncInfo.push(`领先 ${status.ahead} 个提交`)
|
|
507
|
+
if (status.behind > 0) syncInfo.push(`落后 ${status.behind} 个提交`)
|
|
508
|
+
console.log(` 同步: ${syncInfo.join(', ')}`)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
console.log()
|
|
512
|
+
}
|
|
513
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/NoteIndexCache.ts
|
|
3
|
+
*
|
|
4
|
+
* 笔记索引缓存 - 维护笔记的内存索引,避免重复扫描文件系统
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join } from 'path'
|
|
8
|
+
|
|
9
|
+
import { NOTES_PATH } from '../config/constants'
|
|
10
|
+
import { logger } from '../utils'
|
|
11
|
+
|
|
12
|
+
import type { NoteInfo, NoteConfig } from '../types'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 索引项结构
|
|
16
|
+
*/
|
|
17
|
+
interface NoteIndexItem {
|
|
18
|
+
/** 笔记索引(文件夹名前 4 位数字,如 "0001") */
|
|
19
|
+
noteIndex: string
|
|
20
|
+
/** 完整文件夹名称(如 "0001. TNotes 简介") */
|
|
21
|
+
folderName: string
|
|
22
|
+
/** 笔记配置(与 .tnotes.json 结构一致) */
|
|
23
|
+
noteConfig: NoteConfig
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 笔记索引缓存类
|
|
28
|
+
* 提供快速的笔记查询和更新能力
|
|
29
|
+
*/
|
|
30
|
+
export class NoteIndexCache {
|
|
31
|
+
private static instance: NoteIndexCache | null = null
|
|
32
|
+
|
|
33
|
+
/** noteIndex -> NoteIndexItem 的映射 */
|
|
34
|
+
private byNoteIndex: Map<string, NoteIndexItem> = new Map()
|
|
35
|
+
|
|
36
|
+
/** configId (UUID) -> noteIndex 的映射,用于快速反向查询 */
|
|
37
|
+
private byConfigId: Map<string, string> = new Map()
|
|
38
|
+
|
|
39
|
+
/** 是否已完成初始化 */
|
|
40
|
+
private _initialized = false
|
|
41
|
+
|
|
42
|
+
private constructor() {}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 获取单例实例
|
|
46
|
+
*/
|
|
47
|
+
static getInstance(): NoteIndexCache {
|
|
48
|
+
if (!NoteIndexCache.instance) {
|
|
49
|
+
NoteIndexCache.instance = new NoteIndexCache()
|
|
50
|
+
}
|
|
51
|
+
return NoteIndexCache.instance
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 初始化索引缓存
|
|
56
|
+
* @param notes - 扫描得到的笔记列表(已由 NoteManager.scanNotes 完成重复检测)
|
|
57
|
+
*/
|
|
58
|
+
initialize(notes: NoteInfo[]): void {
|
|
59
|
+
this.byNoteIndex.clear()
|
|
60
|
+
this.byConfigId.clear()
|
|
61
|
+
|
|
62
|
+
// 构建索引
|
|
63
|
+
for (const note of notes) {
|
|
64
|
+
const item: NoteIndexItem = {
|
|
65
|
+
noteIndex: note.index,
|
|
66
|
+
folderName: note.dirName,
|
|
67
|
+
noteConfig: note.config,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.byNoteIndex.set(note.index, item)
|
|
71
|
+
this.byConfigId.set(note.config.id, note.index)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this._initialized = true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 是否已完成初始化
|
|
79
|
+
*/
|
|
80
|
+
isInitialized(): boolean {
|
|
81
|
+
return this._initialized
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 从缓存构建 NoteInfo 列表(纯内存,零 I/O)
|
|
86
|
+
* @returns 笔记信息数组
|
|
87
|
+
*/
|
|
88
|
+
toNoteInfoList(): NoteInfo[] {
|
|
89
|
+
const result: NoteInfo[] = []
|
|
90
|
+
for (const item of this.byNoteIndex.values()) {
|
|
91
|
+
const notePath = join(NOTES_PATH, item.folderName)
|
|
92
|
+
result.push({
|
|
93
|
+
index: item.noteIndex,
|
|
94
|
+
path: notePath,
|
|
95
|
+
dirName: item.folderName,
|
|
96
|
+
readmePath: join(notePath, 'README.md'),
|
|
97
|
+
configPath: join(notePath, '.tnotes.json'),
|
|
98
|
+
config: item.noteConfig,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
return result
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 根据 noteIndex 获取索引项
|
|
106
|
+
*/
|
|
107
|
+
getByNoteIndex(noteIndex: string): NoteIndexItem | undefined {
|
|
108
|
+
return this.byNoteIndex.get(noteIndex)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 根据 configId (UUID) 获取索引项
|
|
113
|
+
*/
|
|
114
|
+
getByConfigId(configId: string): NoteIndexItem | undefined {
|
|
115
|
+
const noteIndex = this.byConfigId.get(configId)
|
|
116
|
+
return noteIndex ? this.byNoteIndex.get(noteIndex) : undefined
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 检查 noteIndex 是否存在
|
|
121
|
+
*/
|
|
122
|
+
has(noteIndex: string): boolean {
|
|
123
|
+
return this.byNoteIndex.has(noteIndex)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 更新笔记配置
|
|
128
|
+
* @param noteIndex - 笔记索引
|
|
129
|
+
* @param configUpdates - 要更新的配置字段
|
|
130
|
+
*/
|
|
131
|
+
updateConfig(noteIndex: string, configUpdates: Partial<NoteConfig>): void {
|
|
132
|
+
const item = this.byNoteIndex.get(noteIndex)
|
|
133
|
+
if (!item) {
|
|
134
|
+
logger.warn(`尝试更新不存在的笔记: ${noteIndex}`)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
Object.assign(item.noteConfig, configUpdates)
|
|
139
|
+
item.noteConfig.updated_at = Date.now()
|
|
140
|
+
|
|
141
|
+
logger.debug(`更新笔记配置: ${noteIndex}`, configUpdates)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 删除笔记
|
|
146
|
+
* @param noteIndex - 笔记索引
|
|
147
|
+
*/
|
|
148
|
+
delete(noteIndex: string): void {
|
|
149
|
+
const item = this.byNoteIndex.get(noteIndex)
|
|
150
|
+
if (!item) {
|
|
151
|
+
logger.warn(`尝试删除不存在的笔记: ${noteIndex}`)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 同时删除两个索引
|
|
156
|
+
this.byNoteIndex.delete(noteIndex)
|
|
157
|
+
this.byConfigId.delete(item.noteConfig.id)
|
|
158
|
+
|
|
159
|
+
logger.info(`删除笔记索引: ${noteIndex}`)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 添加新笔记
|
|
164
|
+
* @param note - 笔记信息
|
|
165
|
+
*/
|
|
166
|
+
add(note: NoteInfo): void {
|
|
167
|
+
const item: NoteIndexItem = {
|
|
168
|
+
noteIndex: note.index,
|
|
169
|
+
folderName: note.dirName,
|
|
170
|
+
noteConfig: note.config,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.byNoteIndex.set(note.index, item)
|
|
174
|
+
this.byConfigId.set(note.config.id, note.index)
|
|
175
|
+
|
|
176
|
+
logger.info(`添加笔记索引: ${note.index}`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 更新笔记的文件夹名称(标题变更时)
|
|
181
|
+
* @param noteIndex - 笔记索引
|
|
182
|
+
* @param newFolderName - 新的文件夹名称
|
|
183
|
+
*/
|
|
184
|
+
updateFolderName(noteIndex: string, newFolderName: string): void {
|
|
185
|
+
const item = this.byNoteIndex.get(noteIndex)
|
|
186
|
+
if (!item) {
|
|
187
|
+
logger.warn(`尝试更新不存在的笔记: ${noteIndex}`)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
item.folderName = newFolderName
|
|
192
|
+
logger.debug(`更新笔记文件夹名称: ${noteIndex} -> ${newFolderName}`)
|
|
193
|
+
}
|
|
194
|
+
}
|