@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
package/utils/index.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * utils/index.ts
3
+ *
4
+ * util 统一入口
5
+ */
6
+
7
+ export { handleError, createError } from './errorHandler'
8
+ export { ensureDirectory } from './file'
9
+ export { generateAnchor } from './generateAnchor'
10
+ export { genHierarchicalSidebar } from './genHierarchicalSidebar'
11
+ export { getChangedIds } from './getChangedIds'
12
+ export { LogLevel, Logger, logger, createLogger } from './logger'
13
+ export { createAddNumberToTitle, generateToc } from './markdown'
14
+ export { parseArgs } from './parseArgs'
15
+ export { parseReadmeCompletedNotes } from './parseReadmeCompletedNotes'
16
+ export {
17
+ parseTocLine,
18
+ buildTocLine,
19
+ buildFolderTocLine,
20
+ parseTocToTree,
21
+ parseTocToMutableTree,
22
+ migrateLegacyNoteParents,
23
+ serializeTocTree,
24
+ buildTreeFromFlatNodes,
25
+ parseTocLinesToFlatNodes,
26
+ getSubtreeLineRange,
27
+ getFolderSubtreeRange,
28
+ getTocEntrySubtreeRange,
29
+ collectNoteIndexesInSubtree,
30
+ renameFolderLine,
31
+ getPreviousNoteIndexOutsideSubtree,
32
+ isNoteIndexInSubtree,
33
+ findTocLineIndex,
34
+ findFolderLineIndex,
35
+ findTocEntryLineIndex,
36
+ getPreviousTocNoteIndex,
37
+ processTocEmptyLines,
38
+ parseTocCompletedNotes,
39
+ buildSidebarFromTocTree,
40
+ resolveNoteFromIndex,
41
+ getTocLineCompleted,
42
+ folderTitleFromNoteDirName,
43
+ isTocContentLine,
44
+ TOC_INDENT_SPACES,
45
+ } from './tocHelpers'
46
+ export type {
47
+ ParsedTocLine,
48
+ TocNode,
49
+ TocTreeNode,
50
+ TocFolderNode,
51
+ TocNoteNode,
52
+ TocSidebarItem,
53
+ TocLineKind,
54
+ } from './tocHelpers'
55
+ export {
56
+ migrateReadmeToToc,
57
+ extractReadmeBodyAfterToc,
58
+ README_TOC_END_TAG,
59
+ } from './migrateReadmeToToc'
60
+ export type {
61
+ MigratedTocEntry,
62
+ MigrateReadmeToTocResult,
63
+ } from './migrateReadmeToToc'
64
+ export { isPortInUse, killPortProcess, waitForPort } from './portUtils'
65
+ export {
66
+ parseNoteLine,
67
+ buildNoteLineMarkdown,
68
+ processEmptyLines,
69
+ } from './readmeHelpers'
70
+ export { runCommand } from './runCommand'
71
+ export { validateNoteTitle } from './validators'
@@ -0,0 +1,231 @@
1
+ /**
2
+ * utils/logger.ts
3
+ *
4
+ * 统一的日志系统
5
+ */
6
+
7
+ /**
8
+ * 日志级别枚举
9
+ */
10
+ export enum LogLevel {
11
+ DEBUG = 0,
12
+ INFO = 1,
13
+ WARN = 2,
14
+ ERROR = 3,
15
+ SILENT = 4,
16
+ }
17
+
18
+ /**
19
+ * 日志配置接口
20
+ */
21
+ interface LoggerConfig {
22
+ level?: LogLevel
23
+ prefix?: string
24
+ timestamp?: boolean
25
+ colors?: boolean
26
+ }
27
+
28
+ /**
29
+ * 日志记录器类
30
+ */
31
+ export class Logger {
32
+ private level: LogLevel
33
+ private prefix: string
34
+ private timestamp: boolean
35
+ private colors: boolean
36
+
37
+ constructor(config: LoggerConfig = {}) {
38
+ this.level = config.level ?? LogLevel.INFO
39
+ this.prefix = config.prefix ?? ''
40
+ this.timestamp = config.timestamp ?? false
41
+ this.colors = config.colors ?? true
42
+ }
43
+
44
+ /**
45
+ * 设置日志级别
46
+ */
47
+ setLevel(level: LogLevel): void {
48
+ this.level = level
49
+ }
50
+
51
+ /**
52
+ * 获取当前时间戳(精确到毫秒)
53
+ */
54
+ private getTimestamp(): string {
55
+ if (!this.timestamp) return ''
56
+ const now = new Date()
57
+ const hours = String(now.getHours()).padStart(2, '0')
58
+ const minutes = String(now.getMinutes()).padStart(2, '0')
59
+ const seconds = String(now.getSeconds()).padStart(2, '0')
60
+ const milliseconds = String(now.getMilliseconds()).padStart(3, '0')
61
+ return `[${hours}:${minutes}:${seconds}.${milliseconds}] `
62
+ }
63
+
64
+ /**
65
+ * 格式化日志消息
66
+ */
67
+ private formatMessage(message: string): string {
68
+ const timestamp = this.getTimestamp()
69
+ const prefix = this.prefix ? `[${this.prefix}] ` : ''
70
+ return `${timestamp}${prefix}${message}`
71
+ }
72
+
73
+ /**
74
+ * DEBUG 级别日志
75
+ */
76
+ debug(message: string, ...args: any[]): void {
77
+ if (this.level <= LogLevel.DEBUG) {
78
+ console.log(`🐛 ${this.formatMessage(message)}`, ...args)
79
+ }
80
+ }
81
+
82
+ /**
83
+ * INFO 级别日志
84
+ */
85
+ info(message: string, ...args: any[]): void {
86
+ if (this.level <= LogLevel.INFO) {
87
+ console.log(`ℹ️ ${this.formatMessage(message)}`, ...args)
88
+ }
89
+ }
90
+
91
+ /**
92
+ * 成功日志(特殊的 INFO 级别)
93
+ */
94
+ success(message: string, ...args: any[]): void {
95
+ if (this.level <= LogLevel.INFO) {
96
+ console.log(`✅ ${this.formatMessage(message)}`, ...args)
97
+ }
98
+ }
99
+
100
+ /**
101
+ * 警告日志
102
+ */
103
+ warn(message: string, ...args: any[]): void {
104
+ if (this.level <= LogLevel.WARN) {
105
+ console.warn(`⚠️ ${this.formatMessage(message)}`, ...args)
106
+ }
107
+ }
108
+
109
+ /**
110
+ * 错误日志
111
+ */
112
+ error(message: string, ...args: any[]): void {
113
+ if (this.level <= LogLevel.ERROR) {
114
+ console.error(`❌ ${this.formatMessage(message)}`, ...args)
115
+ }
116
+ }
117
+
118
+ /**
119
+ * 进度日志
120
+ */
121
+ progress(message: string, ...args: any[]): void {
122
+ if (this.level <= LogLevel.INFO) {
123
+ console.log(`⏳ ${this.formatMessage(message)}`, ...args)
124
+ }
125
+ }
126
+
127
+ /**
128
+ * 启动日志
129
+ */
130
+ start(message: string, ...args: any[]): void {
131
+ if (this.level <= LogLevel.INFO) {
132
+ console.log(`🚀 ${this.formatMessage(message)}`, ...args)
133
+ }
134
+ }
135
+
136
+ /**
137
+ * 停止日志
138
+ */
139
+ stop(message: string, ...args: any[]): void {
140
+ if (this.level <= LogLevel.INFO) {
141
+ console.log(`🛑 ${this.formatMessage(message)}`, ...args)
142
+ }
143
+ }
144
+
145
+ /**
146
+ * 完成日志
147
+ */
148
+ done(message: string, duration?: number, ...args: any[]): void {
149
+ if (this.level <= LogLevel.INFO) {
150
+ const durationStr = duration ? ` (${duration}ms)` : ''
151
+ console.log(`✨ ${this.formatMessage(message)}${durationStr}`, ...args)
152
+ }
153
+ }
154
+
155
+ /**
156
+ * 链接日志
157
+ */
158
+ link(message: string, url: string, ...args: any[]): void {
159
+ if (this.level <= LogLevel.INFO) {
160
+ console.log(`🔗 ${this.formatMessage(message)} ${url}`, ...args)
161
+ }
162
+ }
163
+
164
+ /**
165
+ * 文件操作日志
166
+ */
167
+ file(action: string, path: string, ...args: any[]): void {
168
+ if (this.level <= LogLevel.INFO) {
169
+ console.log(`📄 ${this.formatMessage(`${action}: ${path}`)}`, ...args)
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Git 操作日志
175
+ */
176
+ git(message: string, ...args: any[]): void {
177
+ if (this.level <= LogLevel.INFO) {
178
+ console.log(`📦 ${this.formatMessage(message)}`, ...args)
179
+ }
180
+ }
181
+
182
+ /**
183
+ * 创建子 Logger
184
+ */
185
+ child(prefix: string): Logger {
186
+ return new Logger({
187
+ level: this.level,
188
+ prefix: this.prefix ? `${this.prefix}:${prefix}` : prefix,
189
+ timestamp: this.timestamp,
190
+ colors: this.colors,
191
+ })
192
+ }
193
+
194
+ /**
195
+ * 执行函数并记录耗时
196
+ */
197
+ async time<T>(label: string, fn: () => Promise<T>): Promise<T> {
198
+ this.progress(`${label}...`)
199
+ const startTime = Date.now()
200
+
201
+ try {
202
+ const result = await fn()
203
+ const duration = Date.now() - startTime
204
+ this.done(label, duration)
205
+ return result
206
+ } catch (error) {
207
+ const duration = Date.now() - startTime
208
+ this.error(`${label} failed after ${duration}ms`)
209
+ throw error
210
+ }
211
+ }
212
+ }
213
+
214
+ /**
215
+ * 默认 Logger 实例
216
+ */
217
+ export const logger = new Logger({
218
+ level: process.env.DEBUG ? LogLevel.DEBUG : LogLevel.INFO,
219
+ timestamp: true, // 启用时间戳(精确到毫秒)
220
+ colors: true,
221
+ })
222
+
223
+ /**
224
+ * 创建带前缀的 Logger
225
+ */
226
+ export function createLogger(prefix: string, config?: LoggerConfig): Logger {
227
+ return new Logger({
228
+ ...config,
229
+ prefix,
230
+ })
231
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * utils/markdown.ts
3
+ *
4
+ * Markdown 处理工具函数
5
+ */
6
+
7
+ import { generateAnchor } from './generateAnchor'
8
+
9
+ /**
10
+ * 工厂函数,创建一个带有独立编号状态的标题编号生成器
11
+ * @returns 返回 addNumberToTitle 方法
12
+ */
13
+ export function createAddNumberToTitle() {
14
+ const titleNumbers = Array(7).fill(0) // 用于存储每个级别的编号
15
+
16
+ /**
17
+ * 为标题添加编号
18
+ * @param title - 原始标题
19
+ * @returns [新标题, 纯文本标题]
20
+ */
21
+ return function addNumberToTitle(title: string): [string, string] {
22
+ // 正则匹配提取标题信息
23
+ const match = title.match(
24
+ /^(#+)\s*((\d+(\.\d*)?(\.\d*)?(\.\d*)?(\.\d*)?(\.\d*)?)\.\s*)?(.*)/,
25
+ )
26
+ const plainTitle = match ? match[9].trim() : title.trim()
27
+
28
+ const level = title.indexOf(' ')
29
+ const baseLevel = 2 // 基础级别为2
30
+
31
+ // 一级标题不编号
32
+ if (level === 1) return [title, plainTitle]
33
+
34
+ // 重置当前级别以上的编号
35
+ for (let i = level + 1; i < titleNumbers.length; i++) titleNumbers[i] = 0
36
+
37
+ // 增加当前级别的编号
38
+ titleNumbers[level] += 1
39
+
40
+ // 生成新的编号
41
+ const newNumber = titleNumbers.slice(baseLevel, level + 1).join('.')
42
+
43
+ // 构建新的标题
44
+ const headerSymbol = title.slice(0, level).trim() // 获取原有的 # 符号
45
+ const newTitle = `${headerSymbol} ${newNumber}. ${plainTitle}`
46
+
47
+ return [newTitle, plainTitle]
48
+ }
49
+ }
50
+
51
+ /**
52
+ * 生成 Markdown TOC(目录)
53
+ * @param titles - 标题列表
54
+ * @param baseLevel - 基础层级(默认为 2)
55
+ * @param eol - 换行符(默认为 '\n')
56
+ * @returns 生成的 TOC 字符串
57
+ */
58
+ export function generateToc(
59
+ titles: string[],
60
+ baseLevel = 2,
61
+ eol = '\n',
62
+ ): string {
63
+ const toc = titles
64
+ .map((title) => {
65
+ const level = title.indexOf(' ')
66
+ const text = title.slice(level).trim()
67
+ const anchor = generateAnchor(text)
68
+ const indent = Math.max(0, (level - baseLevel) * 2)
69
+ return ' '.repeat(indent) + `- [${text}](#${anchor})`
70
+ })
71
+ .join(eol)
72
+
73
+ // !在 TOC 区域前后添加换行符 - 适配 prettier 格式化
74
+ return `${eol}${toc}${eol}`
75
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * utils/migrateReadmeToToc.test.ts
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+
7
+ import {
8
+ extractReadmeBodyAfterToc,
9
+ migrateReadmeToToc,
10
+ README_TOC_END_TAG,
11
+ } from './migrateReadmeToToc'
12
+
13
+ const TOC_REGION = `<!-- region:toc -->
14
+
15
+ - [1. Alpha](#1-alpha)
16
+
17
+ ${README_TOC_END_TAG}`
18
+
19
+ describe('extractReadmeBodyAfterToc', () => {
20
+ it('returns lines after endregion:toc', () => {
21
+ const content = `${TOC_REGION}\n\n## 1. Alpha\n- [x] [0001. A](url)`
22
+ const lines = extractReadmeBodyAfterToc(content)
23
+ expect(lines.some((l) => l.includes('## 1. Alpha'))).toBe(true)
24
+ expect(lines.some((l) => l.includes('#1-alpha'))).toBe(false)
25
+ })
26
+
27
+ it('throws when endregion:toc is missing', () => {
28
+ expect(() => extractReadmeBodyAfterToc('# no toc')).toThrow(
29
+ README_TOC_END_TAG,
30
+ )
31
+ })
32
+ })
33
+
34
+ describe('migrateReadmeToToc', () => {
35
+ it('outputs folder + nested notes for heading sections', () => {
36
+ const content = `${TOC_REGION}
37
+
38
+ ## 1. One
39
+ - [x] [0001. Alpha](https://github.com/a/b)
40
+ - [ ] [0002. Beta](https://github.com/a/b)
41
+ `
42
+ const { entries } = migrateReadmeToToc(content)
43
+ expect(entries).toEqual([
44
+ { kind: 'folder', folderTitle: '1. One', indent: 0 },
45
+ { kind: 'note', noteIndex: '0001', indent: 1, completed: true },
46
+ { kind: 'note', noteIndex: '0002', indent: 1, completed: false },
47
+ ])
48
+ })
49
+
50
+ it('handles single note section with folder', () => {
51
+ const content = `${TOC_REGION}
52
+
53
+ ## 7. Solo
54
+ - [ ] [0036. Only](https://github.com/a/b)
55
+ `
56
+ const { entries } = migrateReadmeToToc(content)
57
+ expect(entries).toEqual([
58
+ { kind: 'folder', folderTitle: '7. Solo', indent: 0 },
59
+ { kind: 'note', noteIndex: '0036', indent: 1, completed: false },
60
+ ])
61
+ })
62
+
63
+ it('treats ## and ### as separate groups', () => {
64
+ const content = `${TOC_REGION}
65
+
66
+ ## 1. A
67
+ - [x] [0001. Alpha](url)
68
+ - [ ] [0002. Beta](url)
69
+ ### 1.1 B
70
+ - [x] [0003. Gamma](url)
71
+ - [ ] [0004. Delta](url)
72
+ `
73
+ const { entries } = migrateReadmeToToc(content)
74
+ expect(entries).toEqual([
75
+ { kind: 'folder', folderTitle: '1. A', indent: 0 },
76
+ { kind: 'note', noteIndex: '0001', indent: 1, completed: true },
77
+ { kind: 'note', noteIndex: '0002', indent: 1, completed: false },
78
+ { kind: 'folder', folderTitle: '1.1 B', indent: 0 },
79
+ { kind: 'note', noteIndex: '0003', indent: 1, completed: true },
80
+ { kind: 'note', noteIndex: '0004', indent: 1, completed: false },
81
+ ])
82
+ })
83
+
84
+ it('deduplicates note indexes with warnings', () => {
85
+ const content = `${TOC_REGION}
86
+
87
+ ## 1. A
88
+ - [x] [0001. Alpha](url)
89
+ ## 2. B
90
+ - [ ] [0001. Alpha again](url)
91
+ `
92
+ const { entries, warnings } = migrateReadmeToToc(content)
93
+ expect(entries).toHaveLength(2)
94
+ expect(warnings.some((w) => w.includes('0001'))).toBe(true)
95
+ })
96
+
97
+ it('skips empty sections', () => {
98
+ const content = `${TOC_REGION}
99
+
100
+ ## Empty
101
+
102
+ ## 2. B
103
+ - [x] [0002. Beta](url)
104
+ `
105
+ const { entries } = migrateReadmeToToc(content)
106
+ expect(entries).toEqual([
107
+ { kind: 'folder', folderTitle: '2. B', indent: 0 },
108
+ { kind: 'note', noteIndex: '0002', indent: 1, completed: true },
109
+ ])
110
+ })
111
+ })
@@ -0,0 +1,135 @@
1
+ /**
2
+ * utils/migrateReadmeToToc.ts
3
+ *
4
+ * 从 v0.1.x 根目录 README.md 解析笔记分区,生成 TOC.md 条目(init-toc 过渡命令)
5
+ */
6
+
7
+ import { parseTocLine } from './tocHelpers'
8
+
9
+ /** 与 TocGenerator 保持一致 */
10
+ export const README_TOC_END_TAG = '<!-- endregion:toc -->'
11
+
12
+ /** 迁移后的 flat TOC 条目 */
13
+ export type MigratedTocEntry =
14
+ | {
15
+ kind: 'folder'
16
+ folderTitle: string
17
+ indent: number
18
+ }
19
+ | {
20
+ kind: 'note'
21
+ noteIndex: string
22
+ indent: number
23
+ completed: boolean
24
+ }
25
+
26
+ /** migrateReadmeToToc 返回结果 */
27
+ export interface MigrateReadmeToTocResult {
28
+ entries: MigratedTocEntry[]
29
+ warnings: string[]
30
+ }
31
+
32
+ interface NoteGroupItem {
33
+ noteIndex: string
34
+ completed: boolean
35
+ }
36
+
37
+ interface ReadmeSection {
38
+ heading: string | null
39
+ items: NoteGroupItem[]
40
+ }
41
+
42
+ /**
43
+ * 取 `<!-- endregion:toc -->` 之后的 README 正文行
44
+ */
45
+ export function extractReadmeBodyAfterToc(content: string): string[] {
46
+ const endTagIndex = content.indexOf(README_TOC_END_TAG)
47
+ if (endTagIndex === -1) {
48
+ throw new Error(
49
+ `README.md 中未找到 ${README_TOC_END_TAG},无法执行 init-toc 迁移`,
50
+ )
51
+ }
52
+
53
+ const afterTag = content.slice(endTagIndex + README_TOC_END_TAG.length)
54
+ return afterTag.split('\n')
55
+ }
56
+
57
+ /**
58
+ * 从 v0.1.x README 正文解析 TOC 条目。
59
+ *
60
+ * 规则:
61
+ * - 每个 #{2,} 标题独立成组,输出为 folder 行
62
+ * - 组内笔记统一为 folder 下 indent 1 的 note 行
63
+ * - 无标题组内笔记为根级 note 行
64
+ */
65
+ export function migrateReadmeToToc(content: string): MigrateReadmeToTocResult {
66
+ const lines = extractReadmeBodyAfterToc(content)
67
+ const warnings: string[] = []
68
+ const seenIndexes = new Set<string>()
69
+ const sections: ReadmeSection[] = []
70
+ let currentHeading: string | null = null
71
+ let currentGroup: NoteGroupItem[] = []
72
+
73
+ const flushGroup = () => {
74
+ if (currentGroup.length > 0) {
75
+ sections.push({ heading: currentHeading, items: currentGroup })
76
+ }
77
+ currentGroup = []
78
+ }
79
+
80
+ for (const line of lines) {
81
+ const headingMatch = line.match(/^(#{2,})\s+(.+)$/)
82
+ if (headingMatch) {
83
+ flushGroup()
84
+ currentHeading = headingMatch[2].trim()
85
+ continue
86
+ }
87
+
88
+ const parsed = parseTocLine(line)
89
+ if (!parsed.isMatch || !parsed.noteIndex) continue
90
+
91
+ if (seenIndexes.has(parsed.noteIndex)) {
92
+ warnings.push(`跳过重复笔记索引: ${parsed.noteIndex}`)
93
+ continue
94
+ }
95
+
96
+ seenIndexes.add(parsed.noteIndex)
97
+ currentGroup.push({
98
+ noteIndex: parsed.noteIndex,
99
+ completed: parsed.completed,
100
+ })
101
+ }
102
+
103
+ flushGroup()
104
+
105
+ const entries: MigratedTocEntry[] = []
106
+ for (const section of sections) {
107
+ if (section.heading) {
108
+ entries.push({
109
+ kind: 'folder',
110
+ folderTitle: section.heading,
111
+ indent: 0,
112
+ })
113
+ for (const item of section.items) {
114
+ entries.push({
115
+ kind: 'note',
116
+ noteIndex: item.noteIndex,
117
+ indent: 1,
118
+ completed: item.completed,
119
+ })
120
+ }
121
+ continue
122
+ }
123
+
124
+ for (const item of section.items) {
125
+ entries.push({
126
+ kind: 'note',
127
+ noteIndex: item.noteIndex,
128
+ indent: 0,
129
+ completed: item.completed,
130
+ })
131
+ }
132
+ }
133
+
134
+ return { entries, warnings }
135
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * utils/parseArgs.ts
3
+ *
4
+ * 轻量级命令行参数解析器
5
+ * 替代 minimist,仅实现项目需要的功能
6
+ */
7
+
8
+ interface ParsedArgs {
9
+ /** 非选项参数 */
10
+ _: string[]
11
+ /** 选项参数 */
12
+ [key: string]: boolean | string | string[]
13
+ }
14
+
15
+ /**
16
+ * 解析命令行参数
17
+ *
18
+ * 支持的格式:
19
+ * - `--flag` → { flag: true }
20
+ * - `--key=value` → { key: 'value' }
21
+ * - `--key value` → { key: 'value' }(如果 value 不以 - 开头)
22
+ * - `--no-flag` → { flag: false }
23
+ * - 其他参数 → 添加到 `_` 数组
24
+ *
25
+ * @param args 命令行参数数组(通常是 process.argv.slice(2))
26
+ * @returns 解析后的参数对象
27
+ */
28
+ export function parseArgs(args: string[]): ParsedArgs {
29
+ const result: ParsedArgs = { _: [] }
30
+
31
+ for (let i = 0; i < args.length; i++) {
32
+ const arg = args[i]
33
+
34
+ // 处理 --key=value 格式
35
+ if (arg.startsWith('--') && arg.includes('=')) {
36
+ const eqIndex = arg.indexOf('=')
37
+ const key = arg.slice(2, eqIndex)
38
+ const value = arg.slice(eqIndex + 1)
39
+ result[key] = value
40
+ continue
41
+ }
42
+
43
+ // 处理 --no-flag 格式(否定形式)
44
+ if (arg.startsWith('--no-')) {
45
+ const key = arg.slice(5)
46
+ result[key] = false
47
+ continue
48
+ }
49
+
50
+ // 处理 --flag 或 --key value 格式
51
+ if (arg.startsWith('--')) {
52
+ const key = arg.slice(2)
53
+ const next = args[i + 1]
54
+
55
+ // 如果下一个参数存在且不是选项,则作为值
56
+ if (next !== undefined && !next.startsWith('-')) {
57
+ // 检查是否为布尔字符串
58
+ if (next === 'true') {
59
+ result[key] = true
60
+ i++
61
+ } else if (next === 'false') {
62
+ result[key] = false
63
+ i++
64
+ } else {
65
+ // 对于这个项目,所有选项都是布尔值
66
+ // 所以这里保持 --flag 为 true,不消费下一个参数
67
+ result[key] = true
68
+ }
69
+ } else {
70
+ result[key] = true
71
+ }
72
+ continue
73
+ }
74
+
75
+ // 处理短选项 -x(目前项目未使用,但保留基础支持)
76
+ if (arg.startsWith('-') && arg.length > 1 && !arg.startsWith('--')) {
77
+ // 将 -abc 解析为 { a: true, b: true, c: true }
78
+ const flags = arg.slice(1)
79
+ for (const flag of flags) {
80
+ result[flag] = true
81
+ }
82
+ continue
83
+ }
84
+
85
+ // 非选项参数
86
+ result._.push(arg)
87
+ }
88
+
89
+ return result
90
+ }