@tnotesjs/core 0.2.0 → 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.
Files changed (67) hide show
  1. package/config/ConfigManager.ts +137 -0
  2. package/config/constants.ts +121 -0
  3. package/config/index.ts +25 -0
  4. package/config/templates.ts +49 -0
  5. package/core/GitManager.ts +513 -0
  6. package/core/NoteIndexCache.ts +194 -0
  7. package/core/NoteManager.ts +407 -0
  8. package/core/ProcessManager.ts +180 -0
  9. package/core/ReadmeGenerator.ts +215 -0
  10. package/core/TocGenerator.ts +212 -0
  11. package/core/index.ts +11 -0
  12. package/dist/chunk-5QN44ES5.js +6050 -0
  13. package/dist/chunk-5Y5IYS6C.js +200 -0
  14. package/dist/cli/index.js +986 -0
  15. package/dist/index.js +8 -0
  16. package/dist/vitepress/config/index.js +1686 -0
  17. package/package.json +6 -2
  18. package/services/file-watcher/configChangeHandler.ts +64 -0
  19. package/services/file-watcher/eventScheduler.ts +179 -0
  20. package/services/file-watcher/folderChangeHandler.ts +325 -0
  21. package/services/file-watcher/fsWatcherAdapter.ts +128 -0
  22. package/services/file-watcher/globalUpdateCoordinator.ts +60 -0
  23. package/services/file-watcher/index.ts +7 -0
  24. package/services/file-watcher/internal.ts +79 -0
  25. package/services/file-watcher/readmeChangeHandler.ts +28 -0
  26. package/services/file-watcher/renameDetector.ts +120 -0
  27. package/services/file-watcher/service.ts +352 -0
  28. package/services/file-watcher/watchState.ts +194 -0
  29. package/services/git/index.ts +7 -0
  30. package/services/git/service.ts +114 -0
  31. package/services/index.ts +15 -0
  32. package/services/init-sub-repo/index.ts +2 -0
  33. package/services/init-sub-repo/initSubRepoLogic.test.ts +162 -0
  34. package/services/init-sub-repo/initSubRepoLogic.ts +304 -0
  35. package/services/init-sub-repo/service.ts +101 -0
  36. package/services/note/index.ts +7 -0
  37. package/services/note/service.ts +362 -0
  38. package/services/readme/index.ts +7 -0
  39. package/services/readme/service.ts +761 -0
  40. package/services/timestamp/index.ts +7 -0
  41. package/services/timestamp/service.ts +465 -0
  42. package/services/toc/index.ts +5 -0
  43. package/services/toc/moveTocInside.test.ts +73 -0
  44. package/services/toc/service.ts +759 -0
  45. package/services/vitepress/index.ts +7 -0
  46. package/services/vitepress/service.ts +339 -0
  47. package/utils/errorHandler.ts +174 -0
  48. package/utils/file.ts +17 -0
  49. package/utils/genHierarchicalSidebar.ts +69 -0
  50. package/utils/generateAnchor.ts +24 -0
  51. package/utils/getChangedIds.ts +35 -0
  52. package/utils/index.ts +71 -0
  53. package/utils/logger.ts +231 -0
  54. package/utils/markdown.ts +75 -0
  55. package/utils/migrateReadmeToToc.test.ts +111 -0
  56. package/utils/migrateReadmeToToc.ts +135 -0
  57. package/utils/parseArgs.ts +90 -0
  58. package/utils/parseReadmeCompletedNotes.test.ts +90 -0
  59. package/utils/parseReadmeCompletedNotes.ts +108 -0
  60. package/utils/portUtils.ts +113 -0
  61. package/utils/readmeHelpers.ts +190 -0
  62. package/utils/runCommand.ts +29 -0
  63. package/utils/tocHelpers.test.ts +278 -0
  64. package/utils/tocHelpers.ts +855 -0
  65. package/utils/tocNodeId.test.ts +60 -0
  66. package/utils/tocNodeId.ts +97 -0
  67. package/utils/validators.ts +102 -0
@@ -0,0 +1,352 @@
1
+ /**
2
+ * services/file-watcher/service.ts
3
+ *
4
+ * 文件监听服务
5
+ *
6
+ * - 监听笔记文件标题的变化并自动更新 toc
7
+ * - 监听笔记配置文件的变化并自动更新笔记的状态
8
+ */
9
+
10
+ import { request as httpRequest } from 'http'
11
+
12
+ import { ConfigChangeHandler } from './configChangeHandler'
13
+ import { EventScheduler } from './eventScheduler'
14
+ import { FolderChangeHandler } from './folderChangeHandler'
15
+ import { FsWatcherAdapter } from './fsWatcherAdapter'
16
+ import { GlobalUpdateCoordinator } from './globalUpdateCoordinator'
17
+ import { safeExecute } from './internal'
18
+ import { WATCH_EVENT_TYPES } from './internal'
19
+ import { ReadmeChangeHandler } from './readmeChangeHandler'
20
+ import { RenameDetector } from './renameDetector'
21
+ import { WatchState } from './watchState'
22
+ import { NOTES_DIR_PATH, port, repoName } from '../../config/constants'
23
+ import { NoteIndexCache } from '../../core/NoteIndexCache'
24
+ import { logger } from '../../utils'
25
+ import { NoteService } from '../note/service'
26
+ import { ReadmeService } from '../readme/service'
27
+ import { TocService } from '../toc/service'
28
+
29
+ import type { WatchEvent } from './internal'
30
+
31
+ const NOTES_DIR_NOT_SET_ERROR = 'NOTES_DIR_PATH 未设置,无法启动文件监听'
32
+
33
+ const UPDATE_UNLOCK_DELAY_MS = 500
34
+
35
+ /** vite 端 fileWatcherBridgePlugin 暴露的 broadcast 接口路径 */
36
+ const RENAME_BROADCAST_PATH = '/__tnotes_broadcast_rename'
37
+ const SEARCH_REINDEX_PATH = '/__tnotes_search_reindex'
38
+
39
+ export class FileWatcherService {
40
+ private static instance: FileWatcherService | null = null
41
+
42
+ private watchState!: WatchState
43
+ private scheduler!: EventScheduler
44
+ private renameDetector!: RenameDetector
45
+ private configHandler!: ConfigChangeHandler
46
+ private readmeHandler!: ReadmeChangeHandler
47
+ private coordinator!: GlobalUpdateCoordinator
48
+ private folderHandler!: FolderChangeHandler
49
+ private adapter!: FsWatcherAdapter
50
+ private noteService!: NoteService
51
+ private readmeService!: ReadmeService
52
+ private tocService!: TocService
53
+ private noteIndexCache!: NoteIndexCache
54
+ private unlockTimer: NodeJS.Timeout | null = null
55
+
56
+ constructor(private notesDir: string = NOTES_DIR_PATH) {
57
+ if (!this.notesDir) {
58
+ throw new Error(NOTES_DIR_NOT_SET_ERROR)
59
+ }
60
+ this.init()
61
+ FileWatcherService.instance = this
62
+ }
63
+
64
+ static getInstance(): FileWatcherService | null {
65
+ return FileWatcherService.instance
66
+ }
67
+
68
+ private init(): void {
69
+ this.noteService = NoteService.getInstance()
70
+ this.readmeService = ReadmeService.getInstance()
71
+ this.tocService = TocService.getInstance()
72
+ this.noteIndexCache = NoteIndexCache.getInstance()
73
+
74
+ this.watchState = this.initWatchState()
75
+ this.scheduler = this.initScheduler()
76
+ this.folderHandler = this.initFolderHandler()
77
+ this.renameDetector = this.initRenameDetector()
78
+ this.configHandler = this.initConfigHandler()
79
+ this.readmeHandler = this.initReadmeHandler()
80
+ this.coordinator = this.initCoordinator()
81
+ this.adapter = this.initAdapter()
82
+ }
83
+
84
+ private initWatchState(): WatchState {
85
+ const watchState = new WatchState({ notesDir: this.notesDir, logger })
86
+ watchState.initializeFromDisk()
87
+ return watchState
88
+ }
89
+
90
+ private initScheduler(): EventScheduler {
91
+ return new EventScheduler({
92
+ onFlush: (events) => this.handleFileChange(events),
93
+ onPauseForBatch: () => logger.warn('监听服务暂停 3s 等待批量更新完成...'),
94
+ onResumeAfterBatch: () => logger.info('恢复自动监听'),
95
+ reinit: () => this.watchState.initializeFromDisk(),
96
+ })
97
+ }
98
+
99
+ private initFolderHandler(): FolderChangeHandler {
100
+ return new FolderChangeHandler({
101
+ notesDir: this.notesDir,
102
+ watchState: this.watchState,
103
+ scheduler: this.scheduler,
104
+ noteService: this.noteService,
105
+ readmeService: this.readmeService,
106
+ tocService: this.tocService,
107
+ noteIndexCache: this.noteIndexCache,
108
+ logger,
109
+ onRenameSuccess: (payload) => {
110
+ // file-watcher 进程与 Vite dev server 进程不同,无法直接调 ws.send。
111
+ // 通过 HTTP 调 vite 子进程暴露的 broadcast 接口,由其转发为 WS 事件。
112
+ void this.broadcastRename(payload)
113
+ },
114
+ onNoteStructureChanged: (reason) => {
115
+ void this.broadcastSearchReindex(reason)
116
+ },
117
+ })
118
+ }
119
+
120
+ private async broadcastSearchReindex(reason: string): Promise<void> {
121
+ const path = `/${repoName}${SEARCH_REINDEX_PATH}`
122
+ const body = JSON.stringify({ reason })
123
+ const hosts = ['127.0.0.1', '::1']
124
+
125
+ let lastError: unknown = null
126
+ for (const host of hosts) {
127
+ try {
128
+ await this.postOnce(host, path, body)
129
+ return
130
+ } catch (error) {
131
+ lastError = error
132
+ }
133
+ }
134
+ logger.warn(
135
+ `广播搜索索引重建失败 (POST http://[127.0.0.1|::1]:${port}${path}): ${String(lastError)}`,
136
+ )
137
+ }
138
+
139
+ private async broadcastRename(payload: {
140
+ oldFolder: string
141
+ newFolder: string
142
+ noteIndex: string
143
+ }): Promise<void> {
144
+ // 注意:用 Node 原生 http.request,而不是 Node 18 的全局 fetch。
145
+ // Windows 上 undici fetch 在某些 Node 18.x 版本下访问 127.0.0.1 偶发报
146
+ // `TypeError: fetch failed` (cause 通常是 ECONNREFUSED 错误传递异常)。
147
+ // http.request 的行为更可预期,且不依赖 Happy Eyeballs / DNS。
148
+ //
149
+ // 另外:Vite dev server 默认 host=`localhost`,在 Node 18 + Windows 下
150
+ // 可能仅绑定到 IPv6 `::1` 而非 IPv4 `127.0.0.1`,所以这里依次尝试两者。
151
+ const path = `/${repoName}${RENAME_BROADCAST_PATH}`
152
+ const body = JSON.stringify(payload)
153
+ const hosts = ['127.0.0.1', '::1']
154
+
155
+ let lastError: unknown = null
156
+ for (const host of hosts) {
157
+ try {
158
+ await this.postOnce(host, path, body)
159
+ return
160
+ } catch (error) {
161
+ lastError = error
162
+ }
163
+ }
164
+ logger.warn(
165
+ `广播重命名事件失败 (POST http://[127.0.0.1|::1]:${port}${path}): ${String(lastError)}`,
166
+ )
167
+ }
168
+
169
+ private postOnce(host: string, path: string, body: string): Promise<void> {
170
+ return new Promise((resolve, reject) => {
171
+ const req = httpRequest(
172
+ {
173
+ host,
174
+ port,
175
+ path,
176
+ method: 'POST',
177
+ family: host.includes(':') ? 6 : 4,
178
+ headers: {
179
+ 'Content-Type': 'application/json',
180
+ 'Content-Length': Buffer.byteLength(body),
181
+ },
182
+ },
183
+ (res) => {
184
+ // 消费响应体,确保 socket 被释放
185
+ res.on('data', () => {})
186
+ res.on('end', () => {
187
+ if (res.statusCode && res.statusCode >= 400) {
188
+ reject(new Error(`HTTP ${res.statusCode}`))
189
+ return
190
+ }
191
+ resolve()
192
+ })
193
+ },
194
+ )
195
+ req.on('error', reject)
196
+ req.write(body)
197
+ req.end()
198
+ })
199
+ }
200
+
201
+ private initRenameDetector(): RenameDetector {
202
+ return new RenameDetector({
203
+ notesDir: this.notesDir,
204
+ dirCache: {
205
+ has: (name) => this.watchState.hasNoteDir(name),
206
+ add: (name) => this.watchState.addNoteDir(name),
207
+ delete: (name) => this.watchState.deleteNoteDir(name),
208
+ },
209
+ logger,
210
+ onDelete: (oldName) => this.folderHandler.handleFolderDeletion(oldName),
211
+ onRename: (oldName, newName) =>
212
+ this.folderHandler.handleFolderRenameUpdate(oldName, newName),
213
+ })
214
+ }
215
+
216
+ private initConfigHandler(): ConfigChangeHandler {
217
+ return new ConfigChangeHandler({
218
+ state: this.watchState,
219
+ noteService: this.noteService,
220
+ noteIndexCache: this.noteIndexCache,
221
+ logger,
222
+ })
223
+ }
224
+
225
+ private initReadmeHandler(): ReadmeChangeHandler {
226
+ return new ReadmeChangeHandler({ noteService: this.noteService })
227
+ }
228
+
229
+ private initCoordinator(): GlobalUpdateCoordinator {
230
+ return new GlobalUpdateCoordinator({
231
+ readmeService: this.readmeService,
232
+ tocService: this.tocService,
233
+ noteIndexCache: this.noteIndexCache,
234
+ logger,
235
+ })
236
+ }
237
+
238
+ private initAdapter(): FsWatcherAdapter {
239
+ return new FsWatcherAdapter({
240
+ notesDir: this.notesDir,
241
+ isUpdating: () => this.scheduler.getUpdating(),
242
+ onRename: (folderName) => this.renameDetector.handleFsRename(folderName),
243
+ onNoteEvent: (event) => this.onNoteEvent(event),
244
+ logger,
245
+ })
246
+ }
247
+
248
+ start(): void {
249
+ this.watchState.initializeFromDisk()
250
+ this.adapter.start()
251
+ }
252
+
253
+ stop(): void {
254
+ this.adapter.stop()
255
+ this.scheduler.clearTimers()
256
+ this.renameDetector.clearTimers()
257
+ this.folderHandler.clearTimers()
258
+ if (this.unlockTimer) {
259
+ clearTimeout(this.unlockTimer)
260
+ this.unlockTimer = null
261
+ }
262
+ logger.info('文件监听服务已停止')
263
+ }
264
+
265
+ pause(): void {
266
+ this.scheduler.setUpdating(true)
267
+ logger.info('文件监听已暂停')
268
+ }
269
+
270
+ resume(): void {
271
+ this.watchState.initializeFromDisk()
272
+ this.scheduler.setUpdating(false)
273
+ logger.info('文件监听已恢复')
274
+ }
275
+
276
+ isWatching(): boolean {
277
+ return this.adapter.isWatching()
278
+ }
279
+
280
+ /**
281
+ * 挂起文件监听(关闭 fs.watch 句柄),用于需要操作文件夹的场景(如重命名)
282
+ * Windows 上 fs.watch 会锁住文件夹句柄,必须先关闭才能重命名
283
+ */
284
+ suspend(): void {
285
+ this.adapter.stop()
286
+ this.scheduler.setUpdating(true)
287
+ logger.info('文件监听已挂起(fs.watch 已关闭)')
288
+ }
289
+
290
+ /**
291
+ * 恢复文件监听(重新启动 fs.watch)
292
+ */
293
+ unsuspend(): void {
294
+ this.watchState.initializeFromDisk()
295
+ this.scheduler.setUpdating(false)
296
+ this.adapter.start()
297
+ logger.info('文件监听已恢复(fs.watch 已重启)')
298
+ }
299
+
300
+ // #region - 私有实现
301
+
302
+ private onNoteEvent(event: WatchEvent): void {
303
+ if (!this.isNoteFile(event.path)) return
304
+ if (!this.watchState.updateFileHash(event.path)) return
305
+ if (this.scheduler.recordChangeAndDetectBatch()) return
306
+ this.scheduler.enqueue(event)
307
+ }
308
+
309
+ private async handleFileChange(events: WatchEvent[]): Promise<void> {
310
+ try {
311
+ // 优先处理配置状态变更;仅当配置未变更时再处理 README 内容更新
312
+ const configChanges = events.filter(
313
+ (e) => e.type === WATCH_EVENT_TYPES.CONFIG,
314
+ )
315
+ const readmeChanges = events.filter(
316
+ (e) => e.type === WATCH_EVENT_TYPES.README,
317
+ )
318
+
319
+ const changedNoteIndexes = await this.configHandler.handle(configChanges)
320
+
321
+ if (changedNoteIndexes.length > 0) {
322
+ await safeExecute(
323
+ '配置变更更新',
324
+ () => this.coordinator.applyConfigUpdates(changedNoteIndexes),
325
+ logger,
326
+ )
327
+ return
328
+ }
329
+
330
+ await safeExecute(
331
+ 'README 变更更新',
332
+ async () => {
333
+ await this.readmeHandler.handle(readmeChanges)
334
+ await this.coordinator.updateNoteReadmesOnly(events)
335
+ },
336
+ logger,
337
+ )
338
+ } finally {
339
+ if (this.unlockTimer) clearTimeout(this.unlockTimer)
340
+ this.unlockTimer = setTimeout(() => {
341
+ this.unlockTimer = null
342
+ this.scheduler.setUpdating(false)
343
+ }, UPDATE_UNLOCK_DELAY_MS)
344
+ }
345
+ }
346
+
347
+ private isNoteFile(filePath: string): boolean {
348
+ return filePath.endsWith('README.md') || filePath.endsWith('.tnotes.json')
349
+ }
350
+
351
+ // #endregion - 私有实现
352
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * services/file-watcher/watchState.ts
3
+ *
4
+ * 监听状态存储:哈希缓存、配置缓存、目录缓存
5
+ */
6
+
7
+ import { createHash } from 'crypto'
8
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
9
+ import { join } from 'path'
10
+
11
+ import type { ConfigSnapshot } from './internal'
12
+ import type { NoteConfig } from '../../types'
13
+ import type { Logger } from '../../utils'
14
+
15
+ interface WatchStateConfig {
16
+ /** 笔记目录路径 */
17
+ notesDir: string
18
+ /** 日志记录器 */
19
+ logger: Logger
20
+ }
21
+
22
+ export class WatchState {
23
+ /** 文件哈希缓存 */
24
+ private fileHashes = new Map<string, string>()
25
+
26
+ /** 笔记目录缓存 */
27
+ private noteDirCache = new Set<string>()
28
+
29
+ /** 笔记配置缓存 */
30
+ private configCache = new Map<string, ConfigSnapshot>()
31
+
32
+ constructor(private config: WatchStateConfig) {}
33
+
34
+ /**
35
+ * 获取指定文件的 MD5 哈希值,若文件不存在或读取失败返回 null
36
+ *
37
+ * @param filePath 文件路径
38
+ * @returns 文件哈希
39
+ */
40
+ private getFileHash(filePath: string): string | null {
41
+ try {
42
+ if (!existsSync(filePath)) return null
43
+ const content = readFileSync(filePath, 'utf-8')
44
+ // 跳过空内容(可能是其他进程写入时的 truncate 中间状态)
45
+ if (content.length === 0) return null
46
+ return createHash('md5').update(content).digest('hex')
47
+ } catch {
48
+ return null
49
+ }
50
+ }
51
+
52
+ /**
53
+ * 更新文件哈希缓存,只有当文件内容发生变化时才更新并返回 true
54
+ *
55
+ * @param filePath 文件路径
56
+ * @returns 是否发生变化
57
+ */
58
+ updateFileHash(filePath: string): boolean {
59
+ const current = this.getFileHash(filePath)
60
+ if (!current) return false
61
+ const prev = this.fileHashes.get(filePath)
62
+ if (prev === current) return false
63
+ this.fileHashes.set(filePath, current)
64
+ return true
65
+ }
66
+
67
+ /**
68
+ * 检查指定名称的笔记目录是否已存在于缓存中
69
+ *
70
+ * @param name 笔记目录名称
71
+ * @returns 若存在则返回 true,否则返回 false
72
+ */
73
+ hasNoteDir(name: string) {
74
+ return this.noteDirCache.has(name)
75
+ }
76
+
77
+ /**
78
+ * 将指定名称的笔记目录添加到缓存中
79
+ *
80
+ * @param name 笔记目录名称
81
+ */
82
+ addNoteDir(name: string) {
83
+ this.noteDirCache.add(name)
84
+ }
85
+ /**
86
+ * 从缓存中移除指定名称的笔记目录
87
+ *
88
+ * @param name 笔记目录名称
89
+ */
90
+ deleteNoteDir(name: string) {
91
+ this.noteDirCache.delete(name)
92
+ }
93
+
94
+ /**
95
+ * 清空所有缓存数据,包括文件哈希、笔记目录和配置快照
96
+ */
97
+ clearAll(): void {
98
+ this.fileHashes.clear()
99
+ this.noteDirCache.clear()
100
+ this.configCache.clear()
101
+ }
102
+
103
+ /**
104
+ * 清除指定笔记目录相关的缓存数据,包括 README.md 和 .tnotes.json 的文件哈希及配置快照
105
+ *
106
+ * @param noteDirName 笔记目录名称
107
+ */
108
+ clearNoteCaches(noteDirName: string): void {
109
+ const readmePath = join(this.config.notesDir, noteDirName, 'README.md')
110
+ const configPath = join(this.config.notesDir, noteDirName, '.tnotes.json')
111
+ this.fileHashes.delete(readmePath)
112
+ this.fileHashes.delete(configPath)
113
+ this.configCache.delete(configPath)
114
+ }
115
+
116
+ /**
117
+ * 获取指定配置文件路径对应的配置快照
118
+ *
119
+ * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
120
+ * @returns 配置快照,若不存在则返回 undefined
121
+ */
122
+ getConfigSnapshot(configPath: string): ConfigSnapshot | undefined {
123
+ return this.configCache.get(configPath)
124
+ }
125
+
126
+ /**
127
+ * 设置指定配置文件路径的配置快照到缓存中
128
+ *
129
+ * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
130
+ * @param snapshot 配置快照对象
131
+ */
132
+ setConfigSnapshot(configPath: string, snapshot: ConfigSnapshot): void {
133
+ this.configCache.set(configPath, snapshot)
134
+ }
135
+
136
+ /**
137
+ * 读取指定配置文件的快照
138
+ *
139
+ * 解析 .tnotes.json 配置文件,提取 done、enableDiscussions、description 字段。
140
+ *
141
+ * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
142
+ * @returns 配置快照,若文件不存在或解析失败则返回 null
143
+ */
144
+ readConfigSnapshot(configPath: string): ConfigSnapshot | null {
145
+ try {
146
+ if (!existsSync(configPath)) return null
147
+ const content = readFileSync(configPath, 'utf-8')
148
+ const config = JSON.parse(content) as Partial<NoteConfig>
149
+ return {
150
+ done: Boolean(config.done),
151
+ enableDiscussions: Boolean(config.enableDiscussions),
152
+ description: config.description || '',
153
+ }
154
+ } catch (error) {
155
+ this.config.logger.error(`[读取配置快照] ${error}`)
156
+ return null
157
+ }
158
+ }
159
+
160
+ /**
161
+ * 从磁盘初始化监听状态缓存:
162
+ * 遍历笔记根目录下的所有子目录,将每个笔记目录的 README.md 和 .tnotes.json
163
+ * 的哈希值及配置快照加载到缓存中。
164
+ */
165
+ initializeFromDisk(): void {
166
+ try {
167
+ const noteDirs = readdirSync(this.config.notesDir)
168
+ this.clearAll()
169
+
170
+ for (const noteDir of noteDirs) {
171
+ const noteDirPath = join(this.config.notesDir, noteDir)
172
+ if (!statSync(noteDirPath).isDirectory()) continue
173
+
174
+ this.noteDirCache.add(noteDir)
175
+
176
+ const readmePath = join(noteDirPath, 'README.md')
177
+ const readmeHash = this.getFileHash(readmePath)
178
+ if (readmeHash) this.fileHashes.set(readmePath, readmeHash)
179
+
180
+ const configPath = join(noteDirPath, '.tnotes.json')
181
+ const configHash = this.getFileHash(configPath)
182
+ if (configHash) {
183
+ this.fileHashes.set(configPath, configHash)
184
+ const snapshot = this.readConfigSnapshot(configPath)
185
+ if (snapshot) this.configCache.set(configPath, snapshot)
186
+ }
187
+ }
188
+ } catch (error) {
189
+ this.config.logger.warn(
190
+ `[initializeFromDisk] 初始化监听状态失败: ${error}`,
191
+ )
192
+ }
193
+ }
194
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * services/git-service/index.ts
3
+ *
4
+ * Git 服务入口 - 导出 GitService 类
5
+ */
6
+
7
+ export { GitService } from './service'
@@ -0,0 +1,114 @@
1
+ /**
2
+ * services/git-service/service.ts
3
+ *
4
+ * Git 服务 - 封装 Git 操作相关的业务逻辑
5
+ */
6
+
7
+ import { ROOT_DIR_PATH } from '../../config/constants'
8
+ import { GitManager } from '../../core'
9
+ import { logger } from '../../utils'
10
+
11
+ /**
12
+ * Git 推送选项
13
+ */
14
+ interface PushOptions {
15
+ message?: string
16
+ branch?: string
17
+ force?: boolean
18
+ }
19
+
20
+ /**
21
+ * Git 拉取选项
22
+ */
23
+ interface PullOptions {
24
+ branch?: string
25
+ rebase?: boolean
26
+ }
27
+
28
+ /**
29
+ * Git 服务类
30
+ */
31
+ export class GitService {
32
+ private gitManager: GitManager
33
+
34
+ constructor() {
35
+ this.gitManager = new GitManager(ROOT_DIR_PATH)
36
+ }
37
+
38
+ /**
39
+ * 推送到远程仓库
40
+ * @param options - 推送选项
41
+ */
42
+ async push(options: PushOptions = {}): Promise<void> {
43
+ const { message, branch, force = false } = options
44
+
45
+ logger.info('Pushing to remote repository...')
46
+
47
+ if (message) {
48
+ await this.gitManager.pushWithCommit(message, { force })
49
+ } else {
50
+ await this.gitManager.push({ setUpstream: !!branch, force })
51
+ }
52
+
53
+ logger.info('Push completed successfully')
54
+ }
55
+
56
+ /**
57
+ * 从远程仓库拉取
58
+ * @param options - 拉取选项
59
+ */
60
+ async pull(options: PullOptions = {}): Promise<void> {
61
+ const { rebase = false } = options
62
+
63
+ logger.info('Pulling from remote repository...')
64
+
65
+ await this.gitManager.pull({ rebase })
66
+
67
+ logger.info('Pull completed successfully')
68
+ }
69
+
70
+ /**
71
+ * 同步本地和远程仓库(先拉取后推送)
72
+ * @param commitMessage - 可选的提交信息
73
+ */
74
+ async sync(commitMessage?: string): Promise<void> {
75
+ logger.info('Syncing with remote repository...')
76
+
77
+ await this.gitManager.sync({ commitMessage })
78
+
79
+ logger.info('Sync completed successfully')
80
+ }
81
+
82
+ /**
83
+ * 检查是否有未提交的更改
84
+ * @returns 是否有未提交的更改
85
+ */
86
+ async hasChanges(): Promise<boolean> {
87
+ const status = await this.gitManager.getStatus()
88
+ return status.hasChanges
89
+ }
90
+
91
+ /**
92
+ * 生成自动提交信息
93
+ * @returns 自动生成的提交信息
94
+ */
95
+ generateCommitMessage(): string {
96
+ const date = new Date().toISOString().split('T')[0]
97
+ const time = new Date().toTimeString().split(' ')[0]
98
+ return `📝 Update notes - ${date} ${time}`
99
+ }
100
+
101
+ /**
102
+ * 快速提交并推送(使用自动生成的提交信息)
103
+ * @param options - 推送选项
104
+ */
105
+ async quickPush(options: { force?: boolean; skipCheck?: boolean } = {}): Promise<void> {
106
+ if (!options.skipCheck && !(await this.hasChanges())) {
107
+ logger.info('No changes to commit')
108
+ return
109
+ }
110
+
111
+ const message = this.generateCommitMessage()
112
+ await this.push({ message, force: options.force })
113
+ }
114
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * services/index.ts
3
+ *
4
+ * Services 层统一 IO 层
5
+ */
6
+
7
+ export { FileWatcherService } from './file-watcher'
8
+ export { GitService } from './git'
9
+ export { NoteService } from './note'
10
+ export { ReadmeService } from './readme'
11
+ export { TocService } from './toc'
12
+ export { TimestampService } from './timestamp'
13
+ export { VitepressService } from './vitepress'
14
+ export { InitSubRepoService } from './init-sub-repo'
15
+ export * from './init-sub-repo/initSubRepoLogic'
@@ -0,0 +1,2 @@
1
+ export { InitSubRepoService, getCorePackageRoot, getTemplateRoot } from './service'
2
+ export * from './initSubRepoLogic'