@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,90 @@
1
+ /**
2
+ * utils/parseReadmeCompletedNotes.test.ts
3
+ *
4
+ * 测试 parseReadmeCompletedNotes 函数,确保正确解析 README 中的完成状态
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest'
8
+
9
+ import { parseReadmeCompletedNotes } from './parseReadmeCompletedNotes'
10
+
11
+ describe('parseReadmeCompletedNotes', () => {
12
+ const REMOTE = 'https://github.com/owner/repo/notes'
13
+
14
+ it('should count completed and total notes correctly', () => {
15
+ const content = [
16
+ `- [x] [0001. First note](${REMOTE}/0001.first-note/README) ✅`,
17
+ `- [ ] [0002. Second note](${REMOTE}/0002.second-note/README) ❌`,
18
+ `- [x] [0003. Third note](${REMOTE}/0003.third-note/README)`,
19
+ `- [ ] [0004. Fourth note](${REMOTE}/0004.fourth-note/README)`,
20
+ ].join('\n')
21
+
22
+ const result = parseReadmeCompletedNotes(content)
23
+
24
+ expect(result.totalCount).toBe(4)
25
+ expect(result.completedCount).toBe(2)
26
+ })
27
+
28
+ it('should prioritize emoji over checkbox markers', () => {
29
+ const content = [
30
+ `- [ ] [0001. Note with x](${REMOTE}/0001.x/README) ❌`,
31
+ `- [x] [0002. Note with check](${REMOTE}/0002.check/README) ✅`,
32
+ ].join('\n')
33
+
34
+ const result = parseReadmeCompletedNotes(content)
35
+
36
+ expect(result.notes[0].completed).toBe(false) // ❌ takes priority
37
+ expect(result.notes[1].completed).toBe(true) // ✅ takes priority
38
+ })
39
+
40
+ it('should deduplicate identical note indexes with same status', () => {
41
+ const content = [
42
+ `- [x] [0001. First note](${REMOTE}/0001.first/README) ✅`,
43
+ `- [x] [0001. First note dup](${REMOTE}/0001.first/README)`,
44
+ ].join('\n')
45
+
46
+ const result = parseReadmeCompletedNotes(content)
47
+
48
+ expect(result.totalCount).toBe(1)
49
+ })
50
+
51
+ it('should throw when same note index has conflicting statuses', () => {
52
+ const content = [
53
+ `- [x] [0001. First note](${REMOTE}/0001.first/README) ✅`,
54
+ `- [ ] [0001. First note undone](${REMOTE}/0001.first/README)`,
55
+ ].join('\n')
56
+
57
+ expect(() => parseReadmeCompletedNotes(content)).toThrow(
58
+ /相同编号.*不同的完成状态/,
59
+ )
60
+ })
61
+
62
+ it('should ignore lines without note index', () => {
63
+ const content = [
64
+ '# Header',
65
+ '',
66
+ `- [x] [0001. First note](${REMOTE}/0001.first/README)`,
67
+ 'some random text without index',
68
+ `- [ ] [0002. Second note](${REMOTE}/0002.second/README)`,
69
+ ].join('\n')
70
+
71
+ const result = parseReadmeCompletedNotes(content)
72
+
73
+ expect(result.totalCount).toBe(2)
74
+ expect(result.completedCount).toBe(1)
75
+ })
76
+
77
+ it('should handle empty content', () => {
78
+ const result = parseReadmeCompletedNotes('')
79
+
80
+ expect(result.totalCount).toBe(0)
81
+ expect(result.completedCount).toBe(0)
82
+ })
83
+
84
+ it('should handle content with no matching lines', () => {
85
+ const result = parseReadmeCompletedNotes('# Just a header\n\nSome text')
86
+
87
+ expect(result.totalCount).toBe(0)
88
+ expect(result.completedCount).toBe(0)
89
+ })
90
+ })
@@ -0,0 +1,108 @@
1
+ /**
2
+ * utils/parseReadmeCompletedNotes.ts
3
+ *
4
+ * 解析 README.md 中的完成笔记数量
5
+ */
6
+
7
+ /**
8
+ * 笔记状态
9
+ */
10
+ interface NoteStatus {
11
+ noteIndex: string // 笔记编号(如 "0001")
12
+ completed: boolean // 是否完成
13
+ line: string // 原始行内容
14
+ }
15
+
16
+ /**
17
+ * 解析结果
18
+ */
19
+ interface ParseResult {
20
+ completedCount: number // 完成的笔记数量
21
+ totalCount: number // 总笔记数量
22
+ notes: NoteStatus[] // 所有笔记的状态
23
+ }
24
+
25
+ /**
26
+ * 从 README.md 内容中解析完成笔记数量
27
+ *
28
+ * 优先级判断逻辑(从上到下,匹配一个即停止):
29
+ * 1. 如果有 ❌ → 未完成
30
+ * 2. 如果有 ⏰ → 未完成
31
+ * 3. 如果有 ✅ → 完成
32
+ * 4. 如果以 - [ ] 开头 → 未完成
33
+ * 5. 如果以 - [x] 开头 → 完成
34
+ *
35
+ * @param content - README.md 的内容
36
+ * @returns 解析结果
37
+ * @throws 如果发现相同编号的笔记有不同的完成状态
38
+ */
39
+ export function parseReadmeCompletedNotes(content: string): ParseResult {
40
+ const lines = content.split('\n')
41
+ const noteMap = new Map<string, NoteStatus>()
42
+
43
+ // 笔记编号正则:匹配 4 位数字(如 0001, 0002)
44
+ const noteIndexRegex = /\[(\d{4})\./
45
+
46
+ for (const line of lines) {
47
+ // 提取笔记编号
48
+ const match = line.match(noteIndexRegex)
49
+ if (!match) continue
50
+
51
+ const noteIndex = match[1]
52
+
53
+ // 判断完成状态(按优先级)
54
+ let completed: boolean
55
+
56
+ if (line.includes('❌')) {
57
+ // 优先级 1: ❌ 表示未完成
58
+ completed = false
59
+ } else if (line.includes('⏰')) {
60
+ // 优先级 2: ⏰ 表示未完成
61
+ completed = false
62
+ } else if (line.includes('✅')) {
63
+ // 优先级 3: ✅ 表示完成
64
+ completed = true
65
+ } else if (line.trim().startsWith('- [ ]')) {
66
+ // 优先级 4: - [ ] 表示未完成
67
+ completed = false
68
+ } else if (line.trim().startsWith('- [x]')) {
69
+ // 优先级 5: - [x] 表示完成
70
+ completed = true
71
+ } else {
72
+ // 未匹配任何规则,跳过
73
+ continue
74
+ }
75
+
76
+ // 检查是否已存在该编号的笔记
77
+ if (noteMap.has(noteIndex)) {
78
+ const existing = noteMap.get(noteIndex)!
79
+ if (existing.completed !== completed) {
80
+ throw new Error(
81
+ `发现相同编号 ${noteIndex} 的笔记有不同的完成状态:\n` +
82
+ ` 第一次出现: ${existing.line}\n` +
83
+ ` 第二次出现: ${line}`,
84
+ )
85
+ }
86
+ // 状态相同,跳过(去重)
87
+ continue
88
+ }
89
+
90
+ // 记录笔记状态
91
+ noteMap.set(noteIndex, {
92
+ noteIndex,
93
+ completed,
94
+ line: line.trim(),
95
+ })
96
+ }
97
+
98
+ // 统计结果
99
+ const notes = Array.from(noteMap.values())
100
+ const completedCount = notes.filter((note) => note.completed).length
101
+ const totalCount = notes.length
102
+
103
+ return {
104
+ completedCount,
105
+ totalCount,
106
+ notes,
107
+ }
108
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * utils/portUtils.ts
3
+ *
4
+ * 端口管理工具函数
5
+ */
6
+
7
+ import { execSync } from 'child_process'
8
+
9
+ import { logger } from './logger'
10
+
11
+ /**
12
+ * 检查端口是否被占用
13
+ */
14
+ export function isPortInUse(port: number): boolean {
15
+ try {
16
+ // Windows 系统使用 netstat 命令,只检测 LISTENING 状态(忽略 TIME_WAIT)
17
+ if (process.platform === 'win32') {
18
+ const output = execSync(
19
+ `netstat -ano | findstr :${port} | findstr LISTENING`,
20
+ { encoding: 'utf-8', stdio: 'pipe' },
21
+ )
22
+ return output.trim().length > 0
23
+ }
24
+ // Unix-like 系统使用 lsof 命令
25
+ else {
26
+ const output = execSync(`lsof -i :${port}`, {
27
+ encoding: 'utf-8',
28
+ stdio: 'pipe',
29
+ })
30
+ return output.trim().length > 0
31
+ }
32
+ } catch (error) {
33
+ // 如果命令执行失败(没有找到占用),返回 false
34
+ return false
35
+ }
36
+ }
37
+
38
+ /**
39
+ * 获取占用端口的进程 PID
40
+ */
41
+ function getPortPid(port: number): number | null {
42
+ try {
43
+ if (process.platform === 'win32') {
44
+ const output = execSync(`netstat -ano | findstr :${port}`, {
45
+ encoding: 'utf-8',
46
+ stdio: 'pipe',
47
+ })
48
+ const lines = output.trim().split('\n')
49
+ if (lines.length > 0) {
50
+ const match = lines[0].match(/\s+(\d+)\s*$/)
51
+ if (match) {
52
+ return parseInt(match[1])
53
+ }
54
+ }
55
+ } else {
56
+ const output = execSync(`lsof -t -i :${port}`, {
57
+ encoding: 'utf-8',
58
+ stdio: 'pipe',
59
+ })
60
+ const pid = parseInt(output.trim())
61
+ if (!isNaN(pid)) {
62
+ return pid
63
+ }
64
+ }
65
+ } catch (error) {
66
+ // 命令执行失败
67
+ }
68
+ return null
69
+ }
70
+
71
+ /**
72
+ * 终止占用端口的进程
73
+ */
74
+ export function killPortProcess(port: number): boolean {
75
+ const pid = getPortPid(port)
76
+ if (!pid) {
77
+ return false
78
+ }
79
+
80
+ try {
81
+ if (process.platform === 'win32') {
82
+ execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe' })
83
+ } else {
84
+ execSync(`kill -9 ${pid}`, { stdio: 'pipe' })
85
+ }
86
+ logger.info(`已终止占用端口 ${port} 的进程 (PID: ${pid})`)
87
+ return true
88
+ } catch (error) {
89
+ logger.error(
90
+ `终止进程失败 (PID: ${pid}): ${
91
+ error instanceof Error ? error.message : String(error)
92
+ }`,
93
+ )
94
+ return false
95
+ }
96
+ }
97
+
98
+ /**
99
+ * 等待端口可用
100
+ */
101
+ export async function waitForPort(
102
+ port: number,
103
+ timeout: number = 5000,
104
+ ): Promise<boolean> {
105
+ const startTime = Date.now()
106
+ while (Date.now() - startTime < timeout) {
107
+ if (!isPortInUse(port)) {
108
+ return true
109
+ }
110
+ await new Promise((resolve) => setTimeout(resolve, 100))
111
+ }
112
+ return false
113
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * utils/readmeHelpers.ts
3
+ *
4
+ * README 更新的公共辅助函数
5
+ */
6
+
7
+ import { NoteManager } from '../core/NoteManager'
8
+
9
+ import type { NoteInfo } from '../types'
10
+
11
+ /**
12
+ * 笔记行匹配正则表达式
13
+ *
14
+ * 格式:
15
+ * - [x] [0001. 笔记标题]
16
+ * 或
17
+ * - [x] [0001. 笔记标题] (支持缩进)
18
+ *
19
+ * 要求: 笔记名称必须是 4 个数字开头,后面紧跟着一个小数点和一个空格,随后跟着任意标题内容
20
+ */
21
+ const NOTE_LINE_REGEX = /^( *)- \[.\] \[(\d{4}\. .+?)\]/
22
+
23
+ /**
24
+ * 笔记链接解析结果
25
+ */
26
+ interface ParsedNoteLine {
27
+ isMatch: boolean // 是否匹配到笔记行
28
+ noteIndex: string | null // 笔记索引 (如 "0001")
29
+ }
30
+
31
+ /**
32
+ * 解析 home readme 笔记链接行,提取笔记 ID
33
+ * @param line - 要解析的行
34
+ * @returns 解析结果 { isMatch, noteIndex }
35
+ */
36
+ export function parseNoteLine(line: string): ParsedNoteLine {
37
+ // 匹配笔记链接格式: - [x] [0001. xxx](...)
38
+ // 或简单格式: - [ ] [0001. xxx]
39
+ // 支持缩进: " - [x] [0001. xxx]"
40
+ const noteMatch = line.match(NOTE_LINE_REGEX)
41
+
42
+ if (!noteMatch) {
43
+ return {
44
+ isMatch: false,
45
+ noteIndex: null,
46
+ }
47
+ }
48
+
49
+ const [, , text] = noteMatch // 第一个捕获组是缩进,第二个是文本
50
+
51
+ // 提取笔记 ID
52
+ const noteIndex = NoteManager.extractNoteIndex(text)
53
+
54
+ return {
55
+ isMatch: true,
56
+ noteIndex,
57
+ }
58
+ }
59
+
60
+ /**
61
+ * 构建 home readme 笔记链接
62
+ * @param note - 笔记信息
63
+ * @param repoOwner - 仓库所有者
64
+ * @param repoName - 仓库名称
65
+ * @returns 完整的 GitHub URL
66
+ */
67
+ function buildNoteLink(
68
+ note: NoteInfo,
69
+ repoOwner: string,
70
+ repoName: string,
71
+ ): string {
72
+ const encodedDirName = encodeURIComponent(note.dirName)
73
+ return `https://github.com/${repoOwner}/${repoName}/tree/main/notes/${encodedDirName}/README.md`
74
+ }
75
+
76
+ /**
77
+ * 根据笔记配置更新状态
78
+ * @param note - 笔记信息
79
+ * @returns { status, deprecatedMark } 状态字符和弃用标记
80
+ */
81
+ function updateNoteStatus(note: NoteInfo): {
82
+ status: string
83
+ deprecatedMark: string
84
+ } {
85
+ let status = ' ' // 默认未完成
86
+ const deprecatedMark = '' // 弃用标记(已废弃,保留返回值结构以免破坏 API)
87
+
88
+ if (note.config) {
89
+ if (note.config.done) {
90
+ status = 'x' // 完成的笔记,勾选复选框
91
+ }
92
+ }
93
+
94
+ return { status, deprecatedMark }
95
+ }
96
+
97
+ /**
98
+ * 构建完整的笔记行
99
+ * @param note - 笔记信息
100
+ * @param repoOwner - 仓库所有者
101
+ * @param repoName - 仓库名称
102
+ * @returns 完整的 Markdown 行
103
+ */
104
+ export function buildNoteLineMarkdown(
105
+ note: NoteInfo,
106
+ repoOwner: string,
107
+ repoName: string,
108
+ ): string {
109
+ const url = buildNoteLink(note, repoOwner, repoName)
110
+ const { status, deprecatedMark } = updateNoteStatus(note)
111
+ return `- [${status}] [${note.dirName}](${url})${deprecatedMark}`
112
+ }
113
+
114
+ /**
115
+ * 检查是否是笔记列表行
116
+ * @param line - 要检查的行
117
+ * @returns 是否是笔记行
118
+ */
119
+ function isNoteLine(line: string): boolean {
120
+ return NOTE_LINE_REGEX.test(line)
121
+ }
122
+
123
+ /**
124
+ * 合并 home readme 连续空行
125
+ * @param lines - 原始行数组
126
+ * @returns 合并后的行数组
127
+ */
128
+ function mergeConsecutiveEmptyLines(lines: string[]): string[] {
129
+ const result: string[] = []
130
+ let previousLineIsEmpty = false
131
+
132
+ for (const line of lines) {
133
+ const isCurrentLineEmpty = line === ''
134
+
135
+ if (isCurrentLineEmpty) {
136
+ // 只有前一行不是空行时才保留当前空行
137
+ if (!previousLineIsEmpty) {
138
+ result.push(line)
139
+ previousLineIsEmpty = true
140
+ }
141
+ } else {
142
+ result.push(line)
143
+ previousLineIsEmpty = false
144
+ }
145
+ }
146
+
147
+ return result
148
+ }
149
+
150
+ /**
151
+ * 移除相邻 home readme 笔记之间的空行
152
+ * @param lines - 行数组
153
+ * @returns 处理后的行数组
154
+ */
155
+ function removeEmptyLinesBetweenNotes(lines: string[]): string[] {
156
+ const result: string[] = []
157
+
158
+ for (let i = 0; i < lines.length; i++) {
159
+ const currentLine = lines[i]
160
+ const prevLine = i > 0 ? lines[i - 1] : null
161
+ const nextLine = i < lines.length - 1 ? lines[i + 1] : null
162
+
163
+ // 如果当前行是空行,且前后都是笔记,则跳过这个空行
164
+ if (currentLine === '' && prevLine && nextLine) {
165
+ const isPrevLineNote = isNoteLine(prevLine)
166
+ const isNextLineNote = isNoteLine(nextLine)
167
+
168
+ if (isPrevLineNote && isNextLineNote) {
169
+ continue // 跳过笔记之间的空行
170
+ }
171
+ }
172
+
173
+ result.push(currentLine)
174
+ }
175
+
176
+ return result
177
+ }
178
+
179
+ /**
180
+ * 处理 README 中的空行
181
+ * 1. 合并连续空行
182
+ * 2. 移除相邻笔记之间的空行
183
+ * @param lines - 原始行数组
184
+ * @returns 处理后的行数组
185
+ */
186
+ export function processEmptyLines(lines: string[]): string[] {
187
+ const stepOne = mergeConsecutiveEmptyLines(lines)
188
+ const stepTwo = removeEmptyLinesBetweenNotes(stepOne)
189
+ return stepTwo
190
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * utils/runCommand.ts
3
+ *
4
+ * 运行命令的工具函数
5
+ */
6
+
7
+ import { exec } from 'child_process'
8
+
9
+ /**
10
+ * 使用 exec 执行命令
11
+ * @param command - 要执行的命令
12
+ * @param dir - 执行目录
13
+ * @returns Promise<string> 命令输出
14
+ */
15
+ export async function runCommand(
16
+ command: string,
17
+ dir: string
18
+ ): Promise<string> {
19
+ return new Promise((resolve, reject) => {
20
+ exec(command, { cwd: dir }, (error, stdout, stderr) => {
21
+ if (error) {
22
+ console.error(`处理 ${dir} 时出错:${stderr}`)
23
+ reject(error)
24
+ } else {
25
+ resolve(stdout.trim())
26
+ }
27
+ })
28
+ })
29
+ }