@tnotesjs/core 0.6.1 → 0.8.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.
Files changed (92) hide show
  1. package/commands/BaseCommand.ts +58 -0
  2. package/commands/build/BuildCommand.ts +25 -0
  3. package/commands/build/PreviewCommand.ts +29 -0
  4. package/commands/build/index.ts +8 -0
  5. package/commands/dev/DevCommand.ts +75 -0
  6. package/commands/dev/index.ts +7 -0
  7. package/commands/git/PullCommand.ts +25 -0
  8. package/commands/git/PushCommand.ts +64 -0
  9. package/commands/git/index.ts +8 -0
  10. package/commands/index.ts +11 -0
  11. package/commands/init-sub-repo/InitSubRepoCommand.ts +206 -0
  12. package/commands/init-sub-repo/index.ts +1 -0
  13. package/commands/misc/HelpCommand.ts +104 -0
  14. package/commands/misc/index.ts +7 -0
  15. package/commands/models.ts +87 -0
  16. package/commands/note/CreateNoteCommand.ts +160 -0
  17. package/commands/note/RenameNoteCommand.ts +147 -0
  18. package/commands/note/UpdateNoteConfigCommand.ts +78 -0
  19. package/commands/note/index.ts +9 -0
  20. package/commands/registry.ts +47 -0
  21. package/commands/update/UpdateCommand.ts +219 -0
  22. package/commands/update/index.ts +7 -0
  23. package/commands/update-completed-count/UpdateCompletedCountCommand.ts +208 -0
  24. package/commands/update-completed-count/index.ts +5 -0
  25. package/config/templates.ts +0 -5
  26. package/dist/{chunk-5F5RJ2HV.cjs → chunk-7FEANCT6.cjs} +16 -7
  27. package/dist/chunk-AGVER5MX.cjs +1746 -0
  28. package/dist/{chunk-TJ4527KA.cjs → chunk-BL22KD4V.cjs} +324 -950
  29. package/dist/{chunk-56PKXCOX.js → chunk-GWG75A5M.js} +13 -4
  30. package/dist/{chunk-DUTS75WQ.js → chunk-XNO3PCEH.js} +374 -1000
  31. package/dist/chunk-ZEQS64PE.js +1746 -0
  32. package/dist/cli/index.cjs +76 -155
  33. package/dist/cli/index.js +10 -89
  34. package/dist/markdown/index.cjs +8 -12
  35. package/dist/markdown/index.d.cts +1 -1
  36. package/dist/markdown/index.d.ts +1 -1
  37. package/dist/markdown/index.js +7 -11
  38. package/dist/{types-CwBWwNVv.d.ts → types-C_d0fFeD.d.ts} +8 -1
  39. package/dist/{types-CpsEy8lA.d.cts → types-MRBGgJTX.d.cts} +8 -1
  40. package/dist/vitepress/config/index.cjs +402 -178
  41. package/dist/vitepress/config/index.js +392 -168
  42. package/dist/workspace/index.cjs +4 -1202
  43. package/dist/workspace/index.d.cts +2 -2
  44. package/dist/workspace/index.d.ts +2 -2
  45. package/dist/workspace/index.js +4 -1202
  46. package/markdown/components.ts +86 -0
  47. package/markdown/index.ts +17 -0
  48. package/markdown/noteFormatter.test.ts +44 -0
  49. package/markdown/noteFormatter.ts +237 -0
  50. package/package.json +7 -3
  51. package/services/file-watcher/folderChangeHandler.ts +19 -15
  52. package/services/file-watcher/globalUpdateCoordinator.ts +21 -27
  53. package/services/file-watcher/service.ts +1 -6
  54. package/services/index.ts +0 -1
  55. package/services/note/service.ts +4 -6
  56. package/services/readme/service.ts +11 -18
  57. package/services/reconcileToc.ts +20 -0
  58. package/utils/index.ts +0 -9
  59. package/{services/toc → utils}/moveTocInside.test.ts +1 -1
  60. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +9 -18
  61. package/vitepress/components/EnWordList/EnWordList.vue +16 -662
  62. package/vitepress/components/Footprints/Footprints.vue +15 -537
  63. package/vitepress/components/Mermaid/Mermaid.vue +13 -588
  64. package/vitepress/components/MindmapPreview/MindmapPreview.vue +12 -434
  65. package/vitepress/components/MindmapPreview/markdown.ts +1 -1
  66. package/vitepress/components/NotesTable/NotesTable.vue +11 -130
  67. package/vitepress/components/constants.ts +2 -2
  68. package/vitepress/components/sidebar.data.ts +8 -5
  69. package/vitepress/configs/markdown.config.ts +170 -26
  70. package/vitepress/plugins/sidebarStructurePlugin.ts +242 -145
  71. package/vitepress/theme/index.ts +9 -13
  72. package/vitepress/theme/styles/base.scss +15 -0
  73. package/workspace/atomic.ts +113 -0
  74. package/workspace/errors.ts +27 -0
  75. package/workspace/index.ts +40 -0
  76. package/workspace/mutationQueue.ts +28 -0
  77. package/workspace/paths.ts +64 -0
  78. package/workspace/reconcile.test.ts +300 -0
  79. package/workspace/reconcile.ts +95 -0
  80. package/workspace/scanner.ts +292 -0
  81. package/workspace/types.ts +224 -0
  82. package/workspace/workspace.test.ts +333 -0
  83. package/workspace/workspace.ts +1020 -0
  84. package/dist/chunk-3R7QSABB.cjs +0 -502
  85. package/dist/chunk-CZXRQPFJ.js +0 -11
  86. package/dist/chunk-HXED7KVT.cjs +0 -11
  87. package/dist/chunk-U6IBLREL.js +0 -502
  88. package/services/toc/index.ts +0 -5
  89. package/services/toc/service.ts +0 -759
  90. package/utils/migrateReadmeToToc.test.ts +0 -111
  91. package/utils/migrateReadmeToToc.ts +0 -135
  92. package/vitepress/components/EnWordList/RightClickMenu.vue +0 -93
@@ -0,0 +1,333 @@
1
+ import fs from 'node:fs/promises'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+
6
+ import { createWorkspace, WorkspaceError } from './index'
7
+
8
+ const temporaryDirectories: string[] = []
9
+
10
+ afterEach(async () => {
11
+ await Promise.all(
12
+ temporaryDirectories.splice(0).map((directory) =>
13
+ fs.rm(directory, { recursive: true, force: true }),
14
+ ),
15
+ )
16
+ })
17
+
18
+ function knowledgeBaseConfig(id: string, repoName: string) {
19
+ return {
20
+ id,
21
+ author: 'tnotesjs',
22
+ repoName,
23
+ keywords: [repoName],
24
+ sidebarShowNoteId: false,
25
+ ignore_dirs: [],
26
+ socialLinks: [],
27
+ menuItems: [],
28
+ root_item: {
29
+ title: repoName,
30
+ completed_notes_count: {},
31
+ details: repoName,
32
+ link: '/',
33
+ },
34
+ }
35
+ }
36
+
37
+ function noteConfig(id: string, done = false) {
38
+ return {
39
+ bilibili: [],
40
+ tnotes: [],
41
+ yuque: [],
42
+ done,
43
+ enableDiscussions: false,
44
+ description: '',
45
+ id,
46
+ customField: 'preserved',
47
+ }
48
+ }
49
+
50
+ async function createFixture(input?: {
51
+ repoName?: string
52
+ knowledgeBaseId?: string
53
+ notes?: Array<{
54
+ index: string
55
+ title: string
56
+ id: string
57
+ done?: boolean
58
+ content?: string
59
+ }>
60
+ toc?: string
61
+ }) {
62
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'tnotes-core-workspace-'))
63
+ temporaryDirectories.push(root)
64
+ const repoName = input?.repoName ?? 'TNotes.fixture'
65
+ const notes =
66
+ input?.notes ??
67
+ [
68
+ {
69
+ index: '0001',
70
+ title: 'Alpha',
71
+ id: 'note-alpha',
72
+ done: false,
73
+ },
74
+ ]
75
+ await fs.mkdir(path.join(root, 'notes'))
76
+ await fs.writeFile(
77
+ path.join(root, '.tnotes.json'),
78
+ JSON.stringify(
79
+ knowledgeBaseConfig(input?.knowledgeBaseId ?? 'kb-fixture', repoName),
80
+ null,
81
+ 2,
82
+ ),
83
+ )
84
+ await fs.writeFile(path.join(root, 'README.md'), 'legacy root readme\n')
85
+
86
+ for (const note of notes) {
87
+ const noteDirectory = path.join(root, 'notes', `${note.index}. ${note.title}`)
88
+ await fs.mkdir(noteDirectory)
89
+ await fs.writeFile(
90
+ path.join(noteDirectory, '.tnotes.json'),
91
+ `${JSON.stringify(noteConfig(note.id, note.done), null, 2)}\n`,
92
+ )
93
+ await fs.writeFile(
94
+ path.join(noteDirectory, 'README.md'),
95
+ note.content ??
96
+ `# stale title\n\n<!-- region:toc -->\n\n<!-- endregion:toc -->\n\n## Intro\n`,
97
+ )
98
+ }
99
+
100
+ const toc =
101
+ input?.toc ??
102
+ `${notes.map((note) => `- [${note.done ? 'x' : ' '}] ${note.index}. ${note.title}`).join('\n')}\n`
103
+ await fs.writeFile(path.join(root, 'TOC.md'), toc)
104
+ await fs.writeFile(path.join(root, 'sidebar.json'), '[]')
105
+ return root
106
+ }
107
+
108
+ describe('path-injected workspace', () => {
109
+ it('keeps two knowledge bases isolated in the same process', async () => {
110
+ const firstRoot = await createFixture({
111
+ repoName: 'TNotes.first',
112
+ knowledgeBaseId: 'kb-first',
113
+ notes: [{ index: '0001', title: 'First', id: 'first-note' }],
114
+ })
115
+ const secondRoot = await createFixture({
116
+ repoName: 'TNotes.second',
117
+ knowledgeBaseId: 'kb-second',
118
+ notes: [{ index: '0001', title: 'Second', id: 'second-note' }],
119
+ })
120
+ const first = createWorkspace({ rootPath: firstRoot })
121
+ const second = createWorkspace({ rootPath: secondRoot })
122
+
123
+ const [firstSnapshot, secondSnapshot] = await Promise.all([
124
+ first.inspect(),
125
+ second.inspect(),
126
+ ])
127
+ expect(firstSnapshot.id).toBe('kb-first')
128
+ expect(secondSnapshot.id).toBe('kb-second')
129
+ expect(firstSnapshot.notes[0].title).toBe('First')
130
+ expect(secondSnapshot.notes[0].title).toBe('Second')
131
+
132
+ const firstDocument = await first.notes.read('first-note')
133
+ const secondDocument = await second.notes.read('second-note')
134
+ expect(firstDocument.readmePath.startsWith(firstRoot)).toBe(true)
135
+ expect(secondDocument.readmePath.startsWith(secondRoot)).toBe(true)
136
+
137
+ await first.dispose()
138
+ await second.dispose()
139
+ })
140
+
141
+ it('reports an invalid workspace without exiting the process', async () => {
142
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'tnotes-invalid-'))
143
+ temporaryDirectories.push(root)
144
+ const workspace = createWorkspace({ rootPath: root })
145
+ const snapshot = await workspace.inspect()
146
+
147
+ expect(snapshot.health.status).toBe('invalid')
148
+ expect(snapshot.health.diagnostics.map((item) => item.code)).toEqual(
149
+ expect.arrayContaining([
150
+ 'CONFIG_MISSING',
151
+ 'TOC_MISSING',
152
+ 'NOTES_DIRECTORY_MISSING',
153
+ ]),
154
+ )
155
+ await expect(
156
+ workspace.notes.create({ title: 'Blocked' }),
157
+ ).rejects.toMatchObject({ code: 'WORKSPACE_INVALID' })
158
+ })
159
+
160
+ it('formats and saves a note without changing the root README', async () => {
161
+ const root = await createFixture()
162
+ const workspace = createWorkspace({ rootPath: root })
163
+ const before = await workspace.notes.read('note-alpha')
164
+ const rootReadmeBefore = await fs.readFile(path.join(root, 'README.md'), 'utf8')
165
+
166
+ const result = await workspace.notes.save({
167
+ noteUuid: 'note-alpha',
168
+ expectedRevision: before.revision,
169
+ content: `# user title\n\n## Hello\n\n### Child\n\n#### 9.8. Plain\n\n\`\`\`md\n## Not a heading\n\`\`\`\n`,
170
+ })
171
+
172
+ expect(result.value.content).toContain(
173
+ '# [0001. Alpha](https://github.com/tnotesjs/TNotes.fixture/tree/main/notes/0001.%20Alpha)',
174
+ )
175
+ expect(result.value.content).toContain('## 1. Hello')
176
+ expect(result.value.content).toContain('### 1.1. Child')
177
+ expect(result.value.content).toContain('#### Plain')
178
+ expect(result.value.content).toContain('```md\n## Not a heading\n```')
179
+ expect(result.value.content).toContain('<!-- region:toc -->')
180
+ expect(result.value.content).toContain('- [1. Hello](#1-hello)')
181
+ expect(await fs.readFile(path.join(root, 'README.md'), 'utf8')).toBe(
182
+ rootReadmeBefore,
183
+ )
184
+
185
+ await expect(
186
+ workspace.notes.save({
187
+ noteUuid: 'note-alpha',
188
+ expectedRevision: before.revision,
189
+ content: result.value.content,
190
+ }),
191
+ ).rejects.toMatchObject({ code: 'REVISION_CONFLICT' })
192
+ })
193
+
194
+ it('uses the smallest free index and updates only TOC/sidebar', async () => {
195
+ const root = await createFixture({
196
+ notes: [
197
+ { index: '0001', title: 'One', id: 'one' },
198
+ { index: '0003', title: 'Three', id: 'three' },
199
+ ],
200
+ })
201
+ const workspace = createWorkspace({ rootPath: root })
202
+ const rootReadmeBefore = await fs.readFile(path.join(root, 'README.md'), 'utf8')
203
+ const created = await workspace.notes.create({
204
+ title: 'Two',
205
+ placement: {
206
+ type: 'note',
207
+ targetNoteUuid: 'three',
208
+ placement: 'before',
209
+ },
210
+ })
211
+
212
+ expect(created.value.index).toBe('0002')
213
+ expect(created.value.config).not.toHaveProperty('category')
214
+ const toc = await fs.readFile(path.join(root, 'TOC.md'), 'utf8')
215
+ expect(toc.indexOf('0002. Two')).toBeLessThan(toc.indexOf('0003. Three'))
216
+ expect(await fs.readFile(path.join(root, 'README.md'), 'utf8')).toBe(
217
+ rootReadmeBefore,
218
+ )
219
+ })
220
+
221
+ it('renames a note while preserving its UUID and unknown config fields', async () => {
222
+ const root = await createFixture()
223
+ const workspace = createWorkspace({ rootPath: root })
224
+ const before = await workspace.notes.read('note-alpha')
225
+ const rootReadmeBefore = await fs.readFile(path.join(root, 'README.md'), 'utf8')
226
+ const renamed = await workspace.notes.rename({
227
+ noteUuid: before.uuid,
228
+ title: 'Renamed',
229
+ expectedRevision: before.revision,
230
+ })
231
+
232
+ expect(renamed.value.uuid).toBe('note-alpha')
233
+ expect(renamed.value.dirName).toBe('0001. Renamed')
234
+ expect(renamed.value.config.customField).toBe('preserved')
235
+ await expect(fs.access(path.join(root, 'notes', '0001. Alpha'))).rejects.toThrow()
236
+ expect(await fs.readFile(path.join(root, 'TOC.md'), 'utf8')).toContain(
237
+ '0001. Renamed',
238
+ )
239
+ expect(await fs.readFile(path.join(root, 'README.md'), 'utf8')).toBe(
240
+ rootReadmeBefore,
241
+ )
242
+ })
243
+
244
+ it('syncs an externally changed TOC checkbox back to note config', async () => {
245
+ const root = await createFixture()
246
+ const workspace = createWorkspace({ rootPath: root })
247
+ await fs.writeFile(path.join(root, 'TOC.md'), '- [x] 0001. Alpha\n')
248
+ const result = await workspace.reconcileTocCompletion()
249
+
250
+ expect(result.value.notes[0].config.done).toBe(true)
251
+ const config = JSON.parse(
252
+ await fs.readFile(
253
+ path.join(root, 'notes', '0001. Alpha', '.tnotes.json'),
254
+ 'utf8',
255
+ ),
256
+ )
257
+ expect(config.done).toBe(true)
258
+ expect(config.customField).toBe('preserved')
259
+ })
260
+
261
+ it('previews and permanently deletes a TOC subtree', async () => {
262
+ const root = await createFixture({
263
+ notes: [
264
+ { index: '0001', title: 'Parent', id: 'parent' },
265
+ { index: '0002', title: 'Child', id: 'child' },
266
+ { index: '0003', title: 'Keep', id: 'keep' },
267
+ ],
268
+ toc: '- Group\n - [ ] 0001. Parent\n - [ ] 0002. Child\n- [ ] 0003. Keep\n',
269
+ })
270
+ const workspace = createWorkspace({ rootPath: root })
271
+ const preview = await workspace.toc.previewDelete({
272
+ type: 'folder',
273
+ folderPath: ['Group'],
274
+ })
275
+ expect(preview.notes.map((note) => note.noteUuid)).toEqual([
276
+ 'parent',
277
+ 'child',
278
+ ])
279
+ expect(preview.filePaths).toHaveLength(4)
280
+
281
+ const deleted = await workspace.toc.deleteEntry({
282
+ entry: { type: 'folder', folderPath: ['Group'] },
283
+ expectedSnapshotRevision: preview.snapshotRevision,
284
+ })
285
+ expect(deleted.value.notes.map((note) => note.uuid)).toEqual(['keep'])
286
+ expect(await fs.readFile(path.join(root, 'TOC.md'), 'utf8')).toBe(
287
+ '- [ ] 0003. Keep\n',
288
+ )
289
+ })
290
+
291
+ it('writes local attachments with collision-safe names', async () => {
292
+ const root = await createFixture()
293
+ const workspace = createWorkspace({ rootPath: root })
294
+ const first = await workspace.attachments.writeLocal({
295
+ noteUuid: 'note-alpha',
296
+ fileName: 'image.png',
297
+ data: new Uint8Array([1, 2, 3]),
298
+ })
299
+ const second = await workspace.attachments.writeLocal({
300
+ noteUuid: 'note-alpha',
301
+ fileName: 'image.png',
302
+ data: new Uint8Array([4, 5, 6]),
303
+ })
304
+
305
+ expect(first.value.markdownPath).toBe('./assets/image.png')
306
+ expect(second.value.markdownPath).toBe('./assets/image-1.png')
307
+ })
308
+
309
+ it('becomes read-only for a future schema', async () => {
310
+ const root = await createFixture()
311
+ const configPath = path.join(root, '.tnotes.json')
312
+ const config = JSON.parse(await fs.readFile(configPath, 'utf8'))
313
+ config.schemaVersion = 999
314
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2))
315
+ const workspace = createWorkspace({ rootPath: root })
316
+
317
+ expect((await workspace.inspect()).health.status).toBe('future-schema')
318
+ await expect(workspace.notes.read('note-alpha')).resolves.toMatchObject({
319
+ uuid: 'note-alpha',
320
+ })
321
+ const document = await workspace.notes.read('note-alpha')
322
+ await expect(
323
+ workspace.notes.save({
324
+ noteUuid: document.uuid,
325
+ expectedRevision: document.revision,
326
+ content: document.content,
327
+ }),
328
+ ).rejects.toMatchObject({
329
+ code: 'WORKSPACE_READ_ONLY',
330
+ })
331
+ expect(WorkspaceError).toBeTypeOf('function')
332
+ })
333
+ })