@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.
Files changed (62) 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/package.json +5 -2
  13. package/services/file-watcher/configChangeHandler.ts +64 -0
  14. package/services/file-watcher/eventScheduler.ts +179 -0
  15. package/services/file-watcher/folderChangeHandler.ts +325 -0
  16. package/services/file-watcher/fsWatcherAdapter.ts +128 -0
  17. package/services/file-watcher/globalUpdateCoordinator.ts +60 -0
  18. package/services/file-watcher/index.ts +7 -0
  19. package/services/file-watcher/internal.ts +79 -0
  20. package/services/file-watcher/readmeChangeHandler.ts +28 -0
  21. package/services/file-watcher/renameDetector.ts +120 -0
  22. package/services/file-watcher/service.ts +352 -0
  23. package/services/file-watcher/watchState.ts +194 -0
  24. package/services/git/index.ts +7 -0
  25. package/services/git/service.ts +114 -0
  26. package/services/index.ts +15 -0
  27. package/services/init-sub-repo/index.ts +2 -0
  28. package/services/init-sub-repo/initSubRepoLogic.test.ts +162 -0
  29. package/services/init-sub-repo/initSubRepoLogic.ts +304 -0
  30. package/services/init-sub-repo/service.ts +101 -0
  31. package/services/note/index.ts +7 -0
  32. package/services/note/service.ts +362 -0
  33. package/services/readme/index.ts +7 -0
  34. package/services/readme/service.ts +761 -0
  35. package/services/timestamp/index.ts +7 -0
  36. package/services/timestamp/service.ts +465 -0
  37. package/services/toc/index.ts +5 -0
  38. package/services/toc/moveTocInside.test.ts +73 -0
  39. package/services/toc/service.ts +759 -0
  40. package/services/vitepress/index.ts +7 -0
  41. package/services/vitepress/service.ts +339 -0
  42. package/utils/errorHandler.ts +174 -0
  43. package/utils/file.ts +17 -0
  44. package/utils/genHierarchicalSidebar.ts +69 -0
  45. package/utils/generateAnchor.ts +24 -0
  46. package/utils/getChangedIds.ts +35 -0
  47. package/utils/index.ts +71 -0
  48. package/utils/logger.ts +231 -0
  49. package/utils/markdown.ts +75 -0
  50. package/utils/migrateReadmeToToc.test.ts +111 -0
  51. package/utils/migrateReadmeToToc.ts +135 -0
  52. package/utils/parseArgs.ts +90 -0
  53. package/utils/parseReadmeCompletedNotes.test.ts +90 -0
  54. package/utils/parseReadmeCompletedNotes.ts +108 -0
  55. package/utils/portUtils.ts +113 -0
  56. package/utils/readmeHelpers.ts +190 -0
  57. package/utils/runCommand.ts +29 -0
  58. package/utils/tocHelpers.test.ts +278 -0
  59. package/utils/tocHelpers.ts +855 -0
  60. package/utils/tocNodeId.test.ts +60 -0
  61. package/utils/tocNodeId.ts +97 -0
  62. package/utils/validators.ts +102 -0
@@ -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'
@@ -0,0 +1,162 @@
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
2
+ import { tmpdir } from 'os'
3
+ import { join } from 'path'
4
+ import { describe, expect, it, afterEach } from 'vitest'
5
+
6
+ import {
7
+ buildInitContext,
8
+ buildRepoName,
9
+ buildPackageJsonTemplate,
10
+ defaultDisplayName,
11
+ isAlreadyInitialized,
12
+ mergePackageJson,
13
+ renderTocMd,
14
+ resolvePackageJsonMerge,
15
+ validatePort,
16
+ validateTopic,
17
+ } from './initSubRepoLogic'
18
+ import { InitSubRepoService } from './service'
19
+
20
+ describe('initSubRepoLogic', () => {
21
+ const baseInput = {
22
+ topic: 'react',
23
+ displayName: 'React 笔记',
24
+ port: 8000,
25
+ description: '这是一个基于 tnotesjs/core 搭建的 TNotes 知识库。',
26
+ repoUuid: 'repo-uuid',
27
+ noteUuid: 'note-uuid',
28
+ coreVersion: '0.1.28',
29
+ }
30
+
31
+ it('buildRepoName 前缀 TNotes.', () => {
32
+ expect(buildRepoName('react')).toBe('TNotes.react')
33
+ })
34
+
35
+ it('defaultDisplayName 首字母大写', () => {
36
+ expect(defaultDisplayName('react')).toBe('React 笔记')
37
+ })
38
+
39
+ it('validateTopic 校验 slug', () => {
40
+ expect(validateTopic('react')).toBeNull()
41
+ expect(validateTopic('React')).not.toBeNull()
42
+ expect(validateTopic('')).not.toBeNull()
43
+ expect(validateTopic('1bad')).not.toBeNull()
44
+ })
45
+
46
+ it('validatePort 默认值与范围', () => {
47
+ expect(validatePort('')).toEqual({ port: 8000 })
48
+ expect(validatePort('9000')).toEqual({ port: 9000 })
49
+ expect(validatePort('abc').error).toBeTruthy()
50
+ })
51
+
52
+ it('renderTocMd 单行格式', () => {
53
+ const ctx = buildInitContext(baseInput)
54
+ expect(renderTocMd(ctx)).toBe('- [ ] 0001. TNotes.react\n')
55
+ })
56
+
57
+ it('mergePackageJson 保留额外字段', () => {
58
+ const template = buildPackageJsonTemplate('0.1.28')
59
+ const merged = mergePackageJson(
60
+ { name: 'my-repo', license: 'MIT', scripts: { old: 'x' } },
61
+ template,
62
+ )
63
+ expect(merged.name).toBe('my-repo')
64
+ expect(merged.license).toBe('MIT')
65
+ expect(merged.scripts).toEqual(template.scripts)
66
+ expect(merged.dependencies).toEqual(template.dependencies)
67
+ })
68
+
69
+ it('resolvePackageJsonMerge 新建与合并', () => {
70
+ const dir = mkdtempSync(join(tmpdir(), 'init-sub-repo-'))
71
+ const pkgPath = join(dir, 'package.json')
72
+ const template = buildPackageJsonTemplate('0.1.28')
73
+
74
+ const created = resolvePackageJsonMerge(pkgPath, template)
75
+ expect(created.action).toBe('created')
76
+
77
+ writeFileSync(pkgPath, JSON.stringify({ name: 'keep-me' }), 'utf-8')
78
+ const updated = resolvePackageJsonMerge(pkgPath, template)
79
+ expect(updated.action).toBe('updated')
80
+ const parsed = JSON.parse(updated.content) as { name: string }
81
+ expect(parsed.name).toBe('keep-me')
82
+
83
+ rmSync(dir, { recursive: true, force: true })
84
+ })
85
+
86
+ it('isAlreadyInitialized 检测 .tnotes.json', () => {
87
+ const dir = mkdtempSync(join(tmpdir(), 'init-sub-repo-'))
88
+ expect(isAlreadyInitialized(dir)).toBe(false)
89
+
90
+ writeFileSync(join(dir, '.tnotes.json'), '{}', 'utf-8')
91
+ expect(isAlreadyInitialized(dir)).toBe(true)
92
+
93
+ rmSync(dir, { recursive: true, force: true })
94
+ })
95
+ })
96
+
97
+ describe('InitSubRepoService', () => {
98
+ let tempRoot = ''
99
+ const templateRoot = join(process.cwd(), 'templates', 'sub-repo')
100
+
101
+ afterEach(() => {
102
+ if (tempRoot) {
103
+ rmSync(tempRoot, { recursive: true, force: true })
104
+ tempRoot = ''
105
+ }
106
+ })
107
+
108
+ it('writeScaffold 创建文件并跳过已存在项', () => {
109
+ tempRoot = mkdtempSync(join(tmpdir(), 'init-sub-repo-svc-'))
110
+ writeFileSync(join(tempRoot, 'README.md'), '# existing', 'utf-8')
111
+
112
+ const service = new InitSubRepoService(tempRoot, templateRoot)
113
+ const result = service.writeScaffold({
114
+ topic: 'demo',
115
+ displayName: 'Demo 笔记',
116
+ port: 8000,
117
+ description: 'desc',
118
+ repoUuid: 'repo-id',
119
+ noteUuid: 'note-id',
120
+ coreVersion: '0.1.28',
121
+ })
122
+
123
+ expect(result.created).toContain('.tnotes.json')
124
+ expect(result.created).toContain('TOC.md')
125
+ expect(result.skipped).toContain('README.md')
126
+ expect(existsSync(join(tempRoot, '.tnotes.json'))).toBe(true)
127
+
128
+ const config = JSON.parse(
129
+ readFileSync(join(tempRoot, '.tnotes.json'), 'utf-8'),
130
+ ) as { repoName: string; port: number }
131
+ expect(config.repoName).toBe('TNotes.demo')
132
+ expect(config.port).toBe(8000)
133
+ })
134
+
135
+ it('writeScaffold 合并已有 package.json', () => {
136
+ tempRoot = mkdtempSync(join(tmpdir(), 'init-sub-repo-svc-'))
137
+ writeFileSync(
138
+ join(tempRoot, 'package.json'),
139
+ JSON.stringify({ name: 'custom-name', private: true }),
140
+ 'utf-8',
141
+ )
142
+
143
+ const service = new InitSubRepoService(tempRoot, templateRoot)
144
+ const result = service.writeScaffold({
145
+ topic: 'vue',
146
+ displayName: 'Vue 笔记',
147
+ port: 8100,
148
+ description: 'desc',
149
+ repoUuid: 'repo-id',
150
+ noteUuid: 'note-id',
151
+ coreVersion: '0.1.28',
152
+ })
153
+
154
+ expect(result.updated).toContain('package.json')
155
+ const pkg = JSON.parse(
156
+ readFileSync(join(tempRoot, 'package.json'), 'utf-8'),
157
+ ) as { name: string; private: boolean; scripts: Record<string, string> }
158
+ expect(pkg.name).toBe('custom-name')
159
+ expect(pkg.private).toBe(true)
160
+ expect(pkg.scripts['tn:dev']).toBe('tnotes --dev')
161
+ })
162
+ })
@@ -0,0 +1,304 @@
1
+ /**
2
+ * services/init-sub-repo/initSubRepoLogic.ts
3
+ *
4
+ * 子知识库初始化纯函数(便于测试)
5
+ */
6
+
7
+ import { existsSync, readFileSync } from 'fs'
8
+ import { join, resolve } from 'path'
9
+
10
+ import { getDefaultConfig } from '../../config/defaultConfig'
11
+ import {
12
+ generateNoteTitle,
13
+ getNewNoteReadmeBody,
14
+ } from '../../config/templates'
15
+
16
+ import type { NoteConfig, TNotesConfig } from '../../types'
17
+
18
+ export const TOPIC_PATTERN = /^[a-z][a-z0-9-]*$/
19
+ export const DEFAULT_PORT = 8000
20
+ export const DEFAULT_DESCRIPTION =
21
+ '这是一个基于 tnotesjs/core 搭建的 TNotes 知识库。'
22
+ export const GITHUB_ORG = 'tnotesjs'
23
+ export const TNOTES_ROOT_URL = 'https://tnotesjs.github.io/TNotes'
24
+
25
+ /** 静态模板文件(相对 templates/sub-repo/) */
26
+ export const STATIC_TEMPLATE_FILES = [
27
+ '.github/workflows/deploy.yml',
28
+ '.vitepress/theme/index.ts',
29
+ '.vitepress/config.mts',
30
+ '.vitepress/env.d.ts',
31
+ '.vscode/settings.json',
32
+ '.vscode/tnotes.code-snippets',
33
+ 'public/logo.png',
34
+ 'public/favicon.ico',
35
+ '.gitattributes',
36
+ '.gitignore',
37
+ 'tsconfig.json',
38
+ ] as const
39
+
40
+ export interface InitSubRepoInput {
41
+ topic: string
42
+ displayName: string
43
+ port: number
44
+ description: string
45
+ repoUuid: string
46
+ noteUuid: string
47
+ coreVersion: string
48
+ }
49
+
50
+ export interface InitSubRepoContext extends InitSubRepoInput {
51
+ repoName: string
52
+ noteTitle: string
53
+ noteDirName: string
54
+ }
55
+
56
+ export interface PackageJsonTemplate {
57
+ type: 'module'
58
+ scripts: Record<string, string>
59
+ dependencies: Record<string, string>
60
+ devDependencies: Record<string, string>
61
+ }
62
+
63
+ export interface InitSubRepoWriteResult {
64
+ created: string[]
65
+ skipped: string[]
66
+ updated: string[]
67
+ }
68
+
69
+ export function buildRepoName(topic: string): string {
70
+ return `TNotes.${topic}`
71
+ }
72
+
73
+ export function defaultDisplayName(topic: string): string {
74
+ const label = topic.charAt(0).toUpperCase() + topic.slice(1)
75
+ return `${label} 笔记`
76
+ }
77
+
78
+ export function validateTopic(topic: string): string | null {
79
+ const trimmed = topic.trim()
80
+ if (!trimmed) return 'topic 不能为空'
81
+ if (!TOPIC_PATTERN.test(trimmed)) {
82
+ return 'topic 须以小写字母开头,仅含小写字母、数字、连字符'
83
+ }
84
+ return null
85
+ }
86
+
87
+ export function validatePort(raw: string): { port: number; error?: string } {
88
+ const trimmed = raw.trim()
89
+ if (!trimmed) return { port: DEFAULT_PORT }
90
+
91
+ const port = Number.parseInt(trimmed, 10)
92
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
93
+ return { port: DEFAULT_PORT, error: '端口须为 1024–65535 之间的整数,已使用默认值 8000' }
94
+ }
95
+ return { port }
96
+ }
97
+
98
+ export function buildInitContext(input: InitSubRepoInput): InitSubRepoContext {
99
+ const repoName = buildRepoName(input.topic)
100
+ const noteTitle = repoName
101
+ const noteDirName = `0001. ${noteTitle}`
102
+
103
+ return {
104
+ ...input,
105
+ repoName,
106
+ noteTitle,
107
+ noteDirName,
108
+ }
109
+ }
110
+
111
+ export function buildTNotesConfig(ctx: InitSubRepoContext): TNotesConfig {
112
+ const config = getDefaultConfig(ctx.repoName)
113
+
114
+ config.id = ctx.repoUuid
115
+ config.port = ctx.port
116
+ config.root_item = {
117
+ ...config.root_item,
118
+ title: ctx.topic,
119
+ details: ctx.displayName,
120
+ link: `https://tnotesjs.github.io/${ctx.repoName}/`,
121
+ }
122
+ config.menuItems = [
123
+ { text: '🏠 Home', link: '/' },
124
+ { text: '⚙️ Settings', link: '/Settings' },
125
+ { text: '📒 TNotes', link: TNOTES_ROOT_URL },
126
+ {
127
+ text: '📂 TNotes.yuque',
128
+ link: 'https://www.yuque.com/tdahuyou/tnotes.yuque',
129
+ },
130
+ ]
131
+
132
+ return config
133
+ }
134
+
135
+ export function buildNoteConfig(noteUuid: string): NoteConfig {
136
+ return {
137
+ id: noteUuid,
138
+ bilibili: [],
139
+ tnotes: [],
140
+ yuque: [],
141
+ done: false,
142
+ enableDiscussions: false,
143
+ description: '',
144
+ }
145
+ }
146
+
147
+ export function buildRepoNotesUrl(repoName: string): string {
148
+ return `https://github.com/${GITHUB_ORG}/${repoName}/tree/main/notes`
149
+ }
150
+
151
+ export function renderFirstNoteReadme(ctx: InitSubRepoContext): string {
152
+ const noteTitle = generateNoteTitle(
153
+ '0001',
154
+ ctx.noteTitle,
155
+ buildRepoNotesUrl(ctx.repoName),
156
+ )
157
+ return `${noteTitle}\n${getNewNoteReadmeBody()}`
158
+ }
159
+
160
+ export function renderIndexMd(ctx: InitSubRepoContext): string {
161
+ return `---
162
+ layout: home
163
+
164
+ hero:
165
+ name: '${ctx.displayName}'
166
+ image:
167
+ src: /logo.png
168
+ alt: TNotes logo
169
+ ---
170
+
171
+ <SidebarCard pending />
172
+ `
173
+ }
174
+
175
+ export function renderTocMd(ctx: InitSubRepoContext): string {
176
+ return `- [ ] 0001. ${ctx.noteTitle}\n`
177
+ }
178
+
179
+ export function renderRootReadme(ctx: InitSubRepoContext): string {
180
+ return `# ${ctx.repoName}\n\n${ctx.description}\n`
181
+ }
182
+
183
+ export function buildPackageJsonTemplate(
184
+ coreVersion: string,
185
+ ): PackageJsonTemplate {
186
+ return {
187
+ type: 'module',
188
+ scripts: {
189
+ 'tn:build': 'tnotes --build',
190
+ 'tn:create-notes': 'tnotes --create-notes',
191
+ 'tn:dev': 'tnotes --dev',
192
+ 'tn:fix-timestamps': 'tnotes --fix-timestamps',
193
+ 'tn:help': 'tnotes --help',
194
+ 'tn:init-sub-repo': 'tnotes --init-sub-repo',
195
+ 'tn:preview': 'tnotes --preview',
196
+ 'tn:pull': 'tnotes --pull',
197
+ 'tn:push': 'tnotes --push',
198
+ 'tn:update': 'tnotes --update',
199
+ 'tn:update-completed-count': 'tnotes --update-completed-count',
200
+ },
201
+ dependencies: {
202
+ '@tnotesjs/core': `^${coreVersion}`,
203
+ },
204
+ devDependencies: {
205
+ vite: '^7.3.1',
206
+ vitepress: '^1.6.3',
207
+ vue: '^3.5.27',
208
+ },
209
+ }
210
+ }
211
+
212
+ export function mergePackageJson(
213
+ existing: Record<string, unknown>,
214
+ template: PackageJsonTemplate,
215
+ ): Record<string, unknown> {
216
+ return {
217
+ ...existing,
218
+ type: template.type,
219
+ scripts: template.scripts,
220
+ dependencies: template.dependencies,
221
+ devDependencies: template.devDependencies,
222
+ }
223
+ }
224
+
225
+ export interface DynamicFileSpec {
226
+ relativePath: string
227
+ content: string
228
+ }
229
+
230
+ export function buildDynamicFiles(ctx: InitSubRepoContext): DynamicFileSpec[] {
231
+ return [
232
+ {
233
+ relativePath: '.tnotes.json',
234
+ content: `${JSON.stringify(buildTNotesConfig(ctx), null, 2)}\n`,
235
+ },
236
+ {
237
+ relativePath: 'package.json',
238
+ content: `${JSON.stringify(buildPackageJsonTemplate(ctx.coreVersion), null, 2)}\n`,
239
+ },
240
+ {
241
+ relativePath: 'index.md',
242
+ content: renderIndexMd(ctx),
243
+ },
244
+ {
245
+ relativePath: 'TOC.md',
246
+ content: renderTocMd(ctx),
247
+ },
248
+ {
249
+ relativePath: 'README.md',
250
+ content: renderRootReadme(ctx),
251
+ },
252
+ {
253
+ relativePath: join('notes', ctx.noteDirName, 'README.md'),
254
+ content: renderFirstNoteReadme(ctx),
255
+ },
256
+ {
257
+ relativePath: join('notes', ctx.noteDirName, '.tnotes.json'),
258
+ content: `${JSON.stringify(buildNoteConfig(ctx.noteUuid), null, 2)}\n`,
259
+ },
260
+ ]
261
+ }
262
+
263
+ export function isAlreadyInitialized(rootPath: string): boolean {
264
+ return existsSync(resolve(rootPath, '.tnotes.json'))
265
+ }
266
+
267
+ export function getInitializedConfigPath(rootPath: string): string {
268
+ return resolve(rootPath, '.tnotes.json')
269
+ }
270
+
271
+ export function resolvePackageJsonMerge(
272
+ targetPath: string,
273
+ template: PackageJsonTemplate,
274
+ ): { content: string; action: 'created' | 'updated' } {
275
+ if (!existsSync(targetPath)) {
276
+ return {
277
+ content: `${JSON.stringify(template, null, 2)}\n`,
278
+ action: 'created',
279
+ }
280
+ }
281
+
282
+ const existing = JSON.parse(readFileSync(targetPath, 'utf-8')) as Record<
283
+ string,
284
+ unknown
285
+ >
286
+ const merged = mergePackageJson(existing, template)
287
+ return {
288
+ content: `${JSON.stringify(merged, null, 2)}\n`,
289
+ action: 'updated',
290
+ }
291
+ }
292
+
293
+ export function buildManualSteps(ctx: InitSubRepoContext): string[] {
294
+ return [
295
+ 'pnpm install — package.json 已更新,需重装依赖',
296
+ `创建 GitHub 仓库 https://github.com/${GITHUB_ORG}/${ctx.repoName}(若尚未创建)`,
297
+ 'git init / git remote add origin(若尚未关联远程)',
298
+ 'GitHub Pages:Settings → Build and deployment → Source 选 GitHub Actions',
299
+ 'Repository secret:添加 TNOTES_DISPATCH_TOKEN(供 deploy.yml notify job 回调根库)',
300
+ `主题图标(可选):确认 CDN 存在 icon--${ctx.topic}.svg,否则替换 .tnotes.json 中 root_item.icon.src`,
301
+ '本地验证:pnpm tn:dev',
302
+ '首次发布:push 到 main 触发 deploy workflow',
303
+ ]
304
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * services/init-sub-repo/service.ts
3
+ *
4
+ * 子知识库初始化服务
5
+ */
6
+
7
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
8
+ import { dirname, join, resolve } from 'path'
9
+ import { fileURLToPath } from 'url'
10
+
11
+ import {
12
+ buildDynamicFiles,
13
+ buildInitContext,
14
+ buildPackageJsonTemplate,
15
+ resolvePackageJsonMerge,
16
+ STATIC_TEMPLATE_FILES,
17
+ } from './initSubRepoLogic'
18
+
19
+ import type { InitSubRepoInput, InitSubRepoWriteResult } from './initSubRepoLogic'
20
+
21
+ export function getCorePackageRoot(fromModuleUrl: string): string {
22
+ let dir = dirname(fileURLToPath(fromModuleUrl))
23
+
24
+ while (dir !== dirname(dir)) {
25
+ const pkgPath = join(dir, 'package.json')
26
+ if (existsSync(pkgPath)) {
27
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { name?: string }
28
+ if (pkg.name === '@tnotesjs/core') return dir
29
+ }
30
+ dir = dirname(dir)
31
+ }
32
+
33
+ throw new Error('无法定位 @tnotesjs/core 包根目录')
34
+ }
35
+
36
+ export function getTemplateRoot(fromModuleUrl: string): string {
37
+ return join(getCorePackageRoot(fromModuleUrl), 'templates', 'sub-repo')
38
+ }
39
+
40
+ export class InitSubRepoService {
41
+ private rootPath: string
42
+ private templateRoot: string
43
+
44
+ constructor(rootPath: string, templateRoot: string) {
45
+ this.rootPath = rootPath
46
+ this.templateRoot = templateRoot
47
+ }
48
+
49
+ static fromModuleUrl(moduleUrl: string, rootPath = process.cwd()): InitSubRepoService {
50
+ return new InitSubRepoService(rootPath, getTemplateRoot(moduleUrl))
51
+ }
52
+
53
+ writeScaffold(input: InitSubRepoInput): InitSubRepoWriteResult {
54
+ const ctx = buildInitContext(input)
55
+ const created: string[] = []
56
+ const skipped: string[] = []
57
+ const updated: string[] = []
58
+
59
+ for (const relativePath of STATIC_TEMPLATE_FILES) {
60
+ const targetPath = resolve(this.rootPath, relativePath)
61
+ if (existsSync(targetPath)) {
62
+ skipped.push(relativePath)
63
+ continue
64
+ }
65
+
66
+ const sourcePath = join(this.templateRoot, relativePath)
67
+ mkdirSync(dirname(targetPath), { recursive: true })
68
+ copyFileSync(sourcePath, targetPath)
69
+ created.push(relativePath)
70
+ }
71
+
72
+ const packageTemplate = buildPackageJsonTemplate(input.coreVersion)
73
+
74
+ for (const file of buildDynamicFiles(ctx)) {
75
+ const targetPath = resolve(this.rootPath, file.relativePath)
76
+
77
+ if (file.relativePath === 'package.json') {
78
+ const { content, action } = resolvePackageJsonMerge(
79
+ targetPath,
80
+ packageTemplate,
81
+ )
82
+ mkdirSync(dirname(targetPath), { recursive: true })
83
+ writeFileSync(targetPath, content, 'utf-8')
84
+ if (action === 'created') created.push(file.relativePath)
85
+ else updated.push(file.relativePath)
86
+ continue
87
+ }
88
+
89
+ if (existsSync(targetPath)) {
90
+ skipped.push(file.relativePath)
91
+ continue
92
+ }
93
+
94
+ mkdirSync(dirname(targetPath), { recursive: true })
95
+ writeFileSync(targetPath, file.content, 'utf-8')
96
+ created.push(file.relativePath)
97
+ }
98
+
99
+ return { created, skipped, updated }
100
+ }
101
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * services/note/index.ts
3
+ *
4
+ * 笔记服务入口
5
+ */
6
+
7
+ export { NoteService } from './service'