@tnotesjs/core 0.1.2 → 0.1.4

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.

Potentially problematic release.


This version of @tnotesjs/core might be problematic. Click here for more details.

@@ -0,0 +1,4631 @@
1
+ import {
2
+ ConfigManager,
3
+ getConfigManager
4
+ } from "./chunk-UIXF3LPU.js";
5
+
6
+ // commands/models.ts
7
+ var COMMAND_NAMES = {
8
+ BUILD: "build",
9
+ CREATE_NOTES: "create-notes",
10
+ DEV: "dev",
11
+ FIX_TIMESTAMPS: "fix-timestamps",
12
+ HELP: "help",
13
+ PREVIEW: "preview",
14
+ PULL: "pull",
15
+ PUSH: "push",
16
+ RENAME_NOTE: "rename-note",
17
+ SYNC_CORE: "sync-core",
18
+ SYNC: "sync",
19
+ UPDATE: "update",
20
+ UPDATE_COMPLETED_COUNT: "update-completed-count",
21
+ UPDATE_NOTE_CONFIG: "update-note-config"
22
+ };
23
+ var COMMAND_DESCRIPTIONS = {
24
+ [COMMAND_NAMES.DEV]: "\u542F\u52A8\u77E5\u8BC6\u5E93\u5F00\u53D1\u670D\u52A1",
25
+ [COMMAND_NAMES.BUILD]: "\u6784\u5EFA\u77E5\u8BC6\u5E93",
26
+ [COMMAND_NAMES.PREVIEW]: "\u9884\u89C8\u6784\u5EFA\u540E\u7684\u77E5\u8BC6\u5E93",
27
+ [COMMAND_NAMES.UPDATE]: "\u6839\u636E\u7B14\u8BB0\u5185\u5BB9\u66F4\u65B0\u77E5\u8BC6\u5E93",
28
+ [COMMAND_NAMES.UPDATE_COMPLETED_COUNT]: "\u66F4\u65B0\u5B8C\u6210\u7B14\u8BB0\u6570\u91CF\u5386\u53F2\u8BB0\u5F55\uFF08\u8FD1 1 \u5E74\uFF0C\u6700\u8FD1 12 \u4E2A\u6708\uFF09",
29
+ [COMMAND_NAMES.CREATE_NOTES]: "\u65B0\u5EFA\u7B14\u8BB0\uFF08\u652F\u6301\u6279\u91CF\u521B\u5EFA\uFF09",
30
+ [COMMAND_NAMES.PUSH]: "\u5C06\u77E5\u8BC6\u5E93\u63A8\u9001\u5230 GitHub (\u4F7F\u7528 --all \u63A8\u9001\u6240\u6709\u77E5\u8BC6\u5E93)",
31
+ [COMMAND_NAMES.PULL]: "\u5C06 GitHub \u7684\u77E5\u8BC6\u5E93\u62C9\u4E0B\u6765 (\u4F7F\u7528 --all \u62C9\u53D6\u6240\u6709\u77E5\u8BC6\u5E93)",
32
+ [COMMAND_NAMES.SYNC]: "\u540C\u6B65\u672C\u5730\u548C\u8FDC\u7A0B\u7684\u77E5\u8BC6\u5E93\u72B6\u6001 (\u4F7F\u7528 --all \u540C\u6B65\u6240\u6709\u77E5\u8BC6\u5E93)",
33
+ [COMMAND_NAMES.SYNC_CORE]: "\u540C\u6B65\u6240\u6709\u5144\u5F1F\u77E5\u8BC6\u5E93\u7684 tnotesjs/core \u5230\u6700\u65B0\u7248\u672C",
34
+ [COMMAND_NAMES.FIX_TIMESTAMPS]: "\u4FEE\u590D\u6240\u6709\u7B14\u8BB0\u7684\u65F6\u95F4\u6233\uFF08\u57FA\u4E8E git \u5386\u53F2\uFF09",
35
+ [COMMAND_NAMES.UPDATE_NOTE_CONFIG]: "\u66F4\u65B0\u7B14\u8BB0\u914D\u7F6E\u6587\u4EF6",
36
+ [COMMAND_NAMES.RENAME_NOTE]: "\u91CD\u547D\u540D\u7B14\u8BB0",
37
+ [COMMAND_NAMES.HELP]: "\u663E\u793A\u5E2E\u52A9\u4FE1\u606F"
38
+ };
39
+ var COMMAND_OPTIONS = {
40
+ ALL: "all",
41
+ QUIET: "quiet",
42
+ FORCE: "force"
43
+ };
44
+
45
+ // utils/errorHandler.ts
46
+ var TNotesError = class _TNotesError extends Error {
47
+ constructor(message, code = "UNKNOWN" /* UNKNOWN */, context) {
48
+ super(message);
49
+ this.code = code;
50
+ this.context = context;
51
+ this.name = "TNotesError";
52
+ if (Error.captureStackTrace) {
53
+ Error.captureStackTrace(this, _TNotesError);
54
+ }
55
+ }
56
+ };
57
+ function handleError(error, exitOnError = false) {
58
+ if (error instanceof TNotesError) {
59
+ console.error(`\u274C TNotesError`);
60
+ console.error(`\u9519\u8BEF\u7801\uFF1A${error.code}`);
61
+ console.error(`\u9519\u8BEF\u4FE1\u606F\uFF1A${error.message}`);
62
+ if (error.context && Object.keys(error.context).length > 0) {
63
+ console.error("\u9519\u8BEF\u4E0A\u4E0B\u6587\u4FE1\u606F\uFF1A", error.context);
64
+ }
65
+ if (error.stack && process.env.DEBUG) {
66
+ console.error("\u9519\u8BEF\u5806\u6808\u4FE1\u606F\uFF1A", error.stack);
67
+ }
68
+ } else if (error instanceof Error) {
69
+ console.error(`\u274C Error`);
70
+ console.error(`\u9519\u8BEF\u4FE1\u606F\uFF1A${error.message}`);
71
+ if (error.stack && process.env.DEBUG) {
72
+ console.error("\u9519\u8BEF\u5806\u6808\u4FE1\u606F\uFF1A", error.stack);
73
+ }
74
+ } else {
75
+ console.error("\u274C \u672A\u77E5\u9519\u8BEF\uFF1A", error);
76
+ }
77
+ if (exitOnError) {
78
+ process.exit(1);
79
+ }
80
+ }
81
+ var createError = {
82
+ fileNotFound: (path2) => new TNotesError(`\u6587\u4EF6\u672A\u627E\u5230\uFF1A${path2}`, "FILE_NOT_FOUND" /* FILE_NOT_FOUND */, {
83
+ path: path2
84
+ }),
85
+ fileReadError: (path2, originalError) => new TNotesError(`\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25\uFF1A${path2}`, "FILE_READ_ERROR" /* FILE_READ_ERROR */, {
86
+ path: path2,
87
+ originalError: originalError?.message
88
+ }),
89
+ fileWriteError: (path2, originalError) => new TNotesError(`\u5199\u5165\u6587\u4EF6\u5931\u8D25\uFF1A${path2}`, "FILE_WRITE_ERROR" /* FILE_WRITE_ERROR */, {
90
+ path: path2,
91
+ originalError: originalError?.message
92
+ }),
93
+ gitNotRepo: (dir) => new TNotesError(`\u4E0D\u662F\u4E00\u4E2A Git \u4ED3\u5E93\uFF1A${dir}`, "GIT_NOT_REPO" /* GIT_NOT_REPO */, {
94
+ dir
95
+ }),
96
+ gitCommandFailed: (command, dir, originalError) => new TNotesError(`Git \u547D\u4EE4\u5931\u8D25\uFF1A${command}`, "GIT_COMMAND_FAILED" /* GIT_COMMAND_FAILED */, {
97
+ command,
98
+ dir,
99
+ originalError: originalError?.message
100
+ }),
101
+ noteIndexInvalid: (noteIndex) => new TNotesError(
102
+ `\u65E0\u6548\u7684\u7B14\u8BB0\u7D22\u5F15\uFF1A${noteIndex}`,
103
+ "NOTE_INDEX_INVALID" /* NOTE_INDEX_INVALID */,
104
+ {
105
+ noteIndex
106
+ }
107
+ ),
108
+ noteConfigInvalid: (notePath, reason) => new TNotesError(
109
+ `\u65E0\u6548\u7684\u7B14\u8BB0\u914D\u7F6E\uFF1A${notePath}`,
110
+ "NOTE_CONFIG_INVALID" /* NOTE_CONFIG_INVALID */,
111
+ { notePath, reason }
112
+ ),
113
+ configInvalid: (field, reason) => new TNotesError(`\u65E0\u6548\u7684\u914D\u7F6E\u5B57\u6BB5\uFF1A${field}`, "CONFIG_INVALID" /* CONFIG_INVALID */, {
114
+ field,
115
+ reason
116
+ }),
117
+ commandNotFound: (commandName) => new TNotesError(`\u672A\u627E\u5230\u547D\u4EE4\uFF1A${commandName}`, "COMMAND_NOT_FOUND" /* COMMAND_NOT_FOUND */, {
118
+ commandName
119
+ }),
120
+ commandFailed: (commandName, exitCode, originalError) => new TNotesError(`\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF1A${commandName}`, "COMMAND_FAILED" /* COMMAND_FAILED */, {
121
+ commandName,
122
+ exitCode,
123
+ originalError: originalError?.message
124
+ }),
125
+ serverStartFailed: (port2, originalError) => new TNotesError(
126
+ `\u542F\u52A8\u670D\u52A1\u5668\u5931\u8D25\uFF1A\u7AEF\u53E3 ${port2}`,
127
+ "SERVER_START_FAILED" /* SERVER_START_FAILED */,
128
+ { port: port2, originalError: originalError?.message }
129
+ ),
130
+ portInUse: (port2) => new TNotesError(`\u7AEF\u53E3 ${port2} \u5DF2\u88AB\u5360\u7528`, "PORT_IN_USE" /* PORT_IN_USE */, {
131
+ port: port2
132
+ })
133
+ };
134
+
135
+ // utils/logger.ts
136
+ var Logger = class _Logger {
137
+ level;
138
+ prefix;
139
+ timestamp;
140
+ colors;
141
+ constructor(config2 = {}) {
142
+ this.level = config2.level ?? 1 /* INFO */;
143
+ this.prefix = config2.prefix ?? "";
144
+ this.timestamp = config2.timestamp ?? false;
145
+ this.colors = config2.colors ?? true;
146
+ }
147
+ /**
148
+ * 设置日志级别
149
+ */
150
+ setLevel(level) {
151
+ this.level = level;
152
+ }
153
+ /**
154
+ * 获取当前时间戳(精确到毫秒)
155
+ */
156
+ getTimestamp() {
157
+ if (!this.timestamp) return "";
158
+ const now = /* @__PURE__ */ new Date();
159
+ const hours = String(now.getHours()).padStart(2, "0");
160
+ const minutes = String(now.getMinutes()).padStart(2, "0");
161
+ const seconds = String(now.getSeconds()).padStart(2, "0");
162
+ const milliseconds = String(now.getMilliseconds()).padStart(3, "0");
163
+ return `[${hours}:${minutes}:${seconds}.${milliseconds}] `;
164
+ }
165
+ /**
166
+ * 格式化日志消息
167
+ */
168
+ formatMessage(message) {
169
+ const timestamp = this.getTimestamp();
170
+ const prefix = this.prefix ? `[${this.prefix}] ` : "";
171
+ return `${timestamp}${prefix}${message}`;
172
+ }
173
+ /**
174
+ * DEBUG 级别日志
175
+ */
176
+ debug(message, ...args) {
177
+ if (this.level <= 0 /* DEBUG */) {
178
+ console.log(`\u{1F41B} ${this.formatMessage(message)}`, ...args);
179
+ }
180
+ }
181
+ /**
182
+ * INFO 级别日志
183
+ */
184
+ info(message, ...args) {
185
+ if (this.level <= 1 /* INFO */) {
186
+ console.log(`\u2139\uFE0F ${this.formatMessage(message)}`, ...args);
187
+ }
188
+ }
189
+ /**
190
+ * 成功日志(特殊的 INFO 级别)
191
+ */
192
+ success(message, ...args) {
193
+ if (this.level <= 1 /* INFO */) {
194
+ console.log(`\u2705 ${this.formatMessage(message)}`, ...args);
195
+ }
196
+ }
197
+ /**
198
+ * 警告日志
199
+ */
200
+ warn(message, ...args) {
201
+ if (this.level <= 2 /* WARN */) {
202
+ console.warn(`\u26A0\uFE0F ${this.formatMessage(message)}`, ...args);
203
+ }
204
+ }
205
+ /**
206
+ * 错误日志
207
+ */
208
+ error(message, ...args) {
209
+ if (this.level <= 3 /* ERROR */) {
210
+ console.error(`\u274C ${this.formatMessage(message)}`, ...args);
211
+ }
212
+ }
213
+ /**
214
+ * 进度日志
215
+ */
216
+ progress(message, ...args) {
217
+ if (this.level <= 1 /* INFO */) {
218
+ console.log(`\u23F3 ${this.formatMessage(message)}`, ...args);
219
+ }
220
+ }
221
+ /**
222
+ * 启动日志
223
+ */
224
+ start(message, ...args) {
225
+ if (this.level <= 1 /* INFO */) {
226
+ console.log(`\u{1F680} ${this.formatMessage(message)}`, ...args);
227
+ }
228
+ }
229
+ /**
230
+ * 停止日志
231
+ */
232
+ stop(message, ...args) {
233
+ if (this.level <= 1 /* INFO */) {
234
+ console.log(`\u{1F6D1} ${this.formatMessage(message)}`, ...args);
235
+ }
236
+ }
237
+ /**
238
+ * 完成日志
239
+ */
240
+ done(message, duration, ...args) {
241
+ if (this.level <= 1 /* INFO */) {
242
+ const durationStr = duration ? ` (${duration}ms)` : "";
243
+ console.log(`\u2728 ${this.formatMessage(message)}${durationStr}`, ...args);
244
+ }
245
+ }
246
+ /**
247
+ * 链接日志
248
+ */
249
+ link(message, url, ...args) {
250
+ if (this.level <= 1 /* INFO */) {
251
+ console.log(`\u{1F517} ${this.formatMessage(message)} ${url}`, ...args);
252
+ }
253
+ }
254
+ /**
255
+ * 文件操作日志
256
+ */
257
+ file(action, path2, ...args) {
258
+ if (this.level <= 1 /* INFO */) {
259
+ console.log(`\u{1F4C4} ${this.formatMessage(`${action}: ${path2}`)}`, ...args);
260
+ }
261
+ }
262
+ /**
263
+ * Git 操作日志
264
+ */
265
+ git(message, ...args) {
266
+ if (this.level <= 1 /* INFO */) {
267
+ console.log(`\u{1F4E6} ${this.formatMessage(message)}`, ...args);
268
+ }
269
+ }
270
+ /**
271
+ * 创建子 Logger
272
+ */
273
+ child(prefix) {
274
+ return new _Logger({
275
+ level: this.level,
276
+ prefix: this.prefix ? `${this.prefix}:${prefix}` : prefix,
277
+ timestamp: this.timestamp,
278
+ colors: this.colors
279
+ });
280
+ }
281
+ /**
282
+ * 执行函数并记录耗时
283
+ */
284
+ async time(label, fn) {
285
+ this.progress(`${label}...`);
286
+ const startTime = Date.now();
287
+ try {
288
+ const result = await fn();
289
+ const duration = Date.now() - startTime;
290
+ this.done(label, duration);
291
+ return result;
292
+ } catch (error) {
293
+ const duration = Date.now() - startTime;
294
+ this.error(`${label} failed after ${duration}ms`);
295
+ throw error;
296
+ }
297
+ }
298
+ };
299
+ var logger = new Logger({
300
+ level: process.env.DEBUG ? 0 /* DEBUG */ : 1 /* INFO */,
301
+ timestamp: true,
302
+ // 启用时间戳(精确到毫秒)
303
+ colors: true
304
+ });
305
+ function createLogger(prefix, config2) {
306
+ return new Logger({
307
+ ...config2,
308
+ prefix
309
+ });
310
+ }
311
+
312
+ // utils/parseArgs.ts
313
+ function parseArgs(args) {
314
+ const result = { _: [] };
315
+ for (let i = 0; i < args.length; i++) {
316
+ const arg = args[i];
317
+ if (arg.startsWith("--") && arg.includes("=")) {
318
+ const eqIndex = arg.indexOf("=");
319
+ const key = arg.slice(2, eqIndex);
320
+ const value = arg.slice(eqIndex + 1);
321
+ result[key] = value;
322
+ continue;
323
+ }
324
+ if (arg.startsWith("--no-")) {
325
+ const key = arg.slice(5);
326
+ result[key] = false;
327
+ continue;
328
+ }
329
+ if (arg.startsWith("--")) {
330
+ const key = arg.slice(2);
331
+ const next = args[i + 1];
332
+ if (next !== void 0 && !next.startsWith("-")) {
333
+ if (next === "true") {
334
+ result[key] = true;
335
+ i++;
336
+ } else if (next === "false") {
337
+ result[key] = false;
338
+ i++;
339
+ } else {
340
+ result[key] = true;
341
+ }
342
+ } else {
343
+ result[key] = true;
344
+ }
345
+ continue;
346
+ }
347
+ if (arg.startsWith("-") && arg.length > 1 && !arg.startsWith("--")) {
348
+ const flags = arg.slice(1);
349
+ for (const flag of flags) {
350
+ result[flag] = true;
351
+ }
352
+ continue;
353
+ }
354
+ result._.push(arg);
355
+ }
356
+ return result;
357
+ }
358
+
359
+ // utils/file.ts
360
+ import fs from "fs";
361
+ async function ensureDirectory(dir) {
362
+ if (!fs.existsSync(dir)) {
363
+ await fs.promises.mkdir(dir, { recursive: true });
364
+ }
365
+ }
366
+
367
+ // utils/generateAnchor.ts
368
+ import GithubSlugger from "github-slugger";
369
+ var slugger = new GithubSlugger();
370
+ var generateAnchor = (label) => {
371
+ slugger.reset();
372
+ return slugger.slug(label);
373
+ };
374
+
375
+ // utils/genHierarchicalSidebar.ts
376
+ var genHierarchicalSidebar = (itemList, titles, titlesNotesCount, sidebarIsCollapsed) => {
377
+ const stack = [];
378
+ const root = [];
379
+ titles.forEach((title, i) => {
380
+ const match = title.match(/^#+/);
381
+ if (!match) return;
382
+ const level = match[0].length;
383
+ const text = title.replace(/^#+\s*/, "");
384
+ const noteItems = itemList.splice(0, titlesNotesCount[i]);
385
+ const node = {
386
+ text,
387
+ collapsed: sidebarIsCollapsed,
388
+ items: noteItems.length > 0 ? noteItems : []
389
+ };
390
+ if (i === 0 && level === 1) return;
391
+ while (stack.length > 0 && stack[stack.length - 1].level >= level) {
392
+ stack.pop();
393
+ }
394
+ if (stack.length === 0) {
395
+ root.push(node);
396
+ } else {
397
+ const parent = stack[stack.length - 1].node;
398
+ if (!parent.items) parent.items = [];
399
+ parent.items.push(node);
400
+ }
401
+ stack.push({ level, node });
402
+ });
403
+ return root;
404
+ };
405
+
406
+ // utils/getChangedIds.ts
407
+ import path from "path";
408
+ import { execSync } from "child_process";
409
+ function getChangedIds() {
410
+ const changedFiles = execSync(
411
+ `git diff --name-only HEAD -- "notes/[0-9][0-9][0-9][0-9]*/README.md"`
412
+ // 根据当前仓库状态和最近一次提交之间的比较
413
+ ).toString().split(/\r?\n/).filter(Boolean).map((fp) => fp.replace(/^"|"$/g, "")).map((fp) => fp.split("/").join(path.sep));
414
+ const changedIds = changedFiles.map((fp) => {
415
+ const parts = fp.split(path.sep);
416
+ const dirName = parts.find((p, i) => parts[i - 1] === "notes");
417
+ return dirName?.slice(0, 4);
418
+ }).filter((id) => Boolean(id));
419
+ return new Set(changedIds);
420
+ }
421
+
422
+ // utils/getTargetDirs.ts
423
+ import { readdirSync } from "fs";
424
+ import { join } from "path";
425
+ var getTargetDirs = (baseDir, prefix, excludeDirs = []) => {
426
+ try {
427
+ const entries = readdirSync(baseDir, { withFileTypes: true });
428
+ const targetDirs = entries.filter((entry) => entry.isDirectory() && entry.name.startsWith(prefix)).map((entry) => join(baseDir, entry.name)).filter((dir) => !excludeDirs.includes(dir));
429
+ return targetDirs;
430
+ } catch (error) {
431
+ const errorMessage = error instanceof Error ? error.message : String(error);
432
+ console.error(`\u8BFB\u53D6\u76EE\u5F55 ${baseDir} \u65F6\u51FA\u9519\uFF1A${errorMessage}`);
433
+ return [];
434
+ }
435
+ };
436
+
437
+ // utils/markdown.ts
438
+ function createAddNumberToTitle() {
439
+ const titleNumbers = Array(7).fill(0);
440
+ return function addNumberToTitle(title) {
441
+ const match = title.match(
442
+ /^(\#+)\s*((\d+(\.\d*)?(\.\d*)?(\.\d*)?(\.\d*)?(\.\d*)?)\.\s*)?(.*)/
443
+ );
444
+ const plainTitle = match ? match[9].trim() : title.trim();
445
+ const level = title.indexOf(" ");
446
+ const baseLevel = 2;
447
+ if (level === 1) return [title, plainTitle];
448
+ for (let i = level + 1; i < titleNumbers.length; i++) titleNumbers[i] = 0;
449
+ titleNumbers[level] += 1;
450
+ const newNumber = titleNumbers.slice(baseLevel, level + 1).join(".");
451
+ const headerSymbol = title.slice(0, level).trim();
452
+ const newTitle = `${headerSymbol} ${newNumber}. ${plainTitle}`;
453
+ return [newTitle, plainTitle];
454
+ };
455
+ }
456
+ function generateToc(titles, baseLevel = 2, eol = "\n") {
457
+ const toc = titles.map((title) => {
458
+ const level = title.indexOf(" ");
459
+ const text = title.slice(level).trim();
460
+ const anchor = generateAnchor(text);
461
+ const indent = Math.max(0, (level - baseLevel) * 2);
462
+ return " ".repeat(indent) + `- [${text}](#${anchor})`;
463
+ }).join(eol);
464
+ return `${eol}${toc}${eol}`;
465
+ }
466
+
467
+ // utils/parseReadmeCompletedNotes.ts
468
+ function parseReadmeCompletedNotes(content) {
469
+ const lines = content.split("\n");
470
+ const noteMap = /* @__PURE__ */ new Map();
471
+ const noteIndexRegex = /\[(\d{4})\./;
472
+ for (const line of lines) {
473
+ const match = line.match(noteIndexRegex);
474
+ if (!match) continue;
475
+ const noteIndex = match[1];
476
+ let completed;
477
+ if (line.includes("\u274C")) {
478
+ completed = false;
479
+ } else if (line.includes("\u23F0")) {
480
+ completed = false;
481
+ } else if (line.includes("\u2705")) {
482
+ completed = true;
483
+ } else if (line.trim().startsWith("- [ ]")) {
484
+ completed = false;
485
+ } else if (line.trim().startsWith("- [x]")) {
486
+ completed = true;
487
+ } else {
488
+ continue;
489
+ }
490
+ if (noteMap.has(noteIndex)) {
491
+ const existing = noteMap.get(noteIndex);
492
+ if (existing.completed !== completed) {
493
+ throw new Error(
494
+ `\u53D1\u73B0\u76F8\u540C\u7F16\u53F7 ${noteIndex} \u7684\u7B14\u8BB0\u6709\u4E0D\u540C\u7684\u5B8C\u6210\u72B6\u6001:
495
+ \u7B2C\u4E00\u6B21\u51FA\u73B0: ${existing.line}
496
+ \u7B2C\u4E8C\u6B21\u51FA\u73B0: ${line}`
497
+ );
498
+ }
499
+ continue;
500
+ }
501
+ noteMap.set(noteIndex, {
502
+ noteIndex,
503
+ completed,
504
+ line: line.trim()
505
+ });
506
+ }
507
+ const notes = Array.from(noteMap.values());
508
+ const completedCount = notes.filter((note) => note.completed).length;
509
+ const totalCount = notes.length;
510
+ return {
511
+ completedCount,
512
+ totalCount,
513
+ notes
514
+ };
515
+ }
516
+
517
+ // utils/portUtils.ts
518
+ import { execSync as execSync2 } from "child_process";
519
+ function isPortInUse(port2) {
520
+ try {
521
+ if (process.platform === "win32") {
522
+ const output = execSync2(
523
+ `netstat -ano | findstr :${port2} | findstr LISTENING`,
524
+ { encoding: "utf-8", stdio: "pipe" }
525
+ );
526
+ return output.trim().length > 0;
527
+ } else {
528
+ const output = execSync2(`lsof -i :${port2}`, {
529
+ encoding: "utf-8",
530
+ stdio: "pipe"
531
+ });
532
+ return output.trim().length > 0;
533
+ }
534
+ } catch (error) {
535
+ return false;
536
+ }
537
+ }
538
+ function getPortPid(port2) {
539
+ try {
540
+ if (process.platform === "win32") {
541
+ const output = execSync2(`netstat -ano | findstr :${port2}`, {
542
+ encoding: "utf-8",
543
+ stdio: "pipe"
544
+ });
545
+ const lines = output.trim().split("\n");
546
+ if (lines.length > 0) {
547
+ const match = lines[0].match(/\s+(\d+)\s*$/);
548
+ if (match) {
549
+ return parseInt(match[1]);
550
+ }
551
+ }
552
+ } else {
553
+ const output = execSync2(`lsof -t -i :${port2}`, {
554
+ encoding: "utf-8",
555
+ stdio: "pipe"
556
+ });
557
+ const pid = parseInt(output.trim());
558
+ if (!isNaN(pid)) {
559
+ return pid;
560
+ }
561
+ }
562
+ } catch (error) {
563
+ }
564
+ return null;
565
+ }
566
+ function killPortProcess(port2) {
567
+ const pid = getPortPid(port2);
568
+ if (!pid) {
569
+ return false;
570
+ }
571
+ try {
572
+ if (process.platform === "win32") {
573
+ execSync2(`taskkill /F /PID ${pid}`, { stdio: "pipe" });
574
+ } else {
575
+ execSync2(`kill -9 ${pid}`, { stdio: "pipe" });
576
+ }
577
+ logger.info(`\u5DF2\u7EC8\u6B62\u5360\u7528\u7AEF\u53E3 ${port2} \u7684\u8FDB\u7A0B (PID: ${pid})`);
578
+ return true;
579
+ } catch (error) {
580
+ logger.error(
581
+ `\u7EC8\u6B62\u8FDB\u7A0B\u5931\u8D25 (PID: ${pid}): ${error instanceof Error ? error.message : String(error)}`
582
+ );
583
+ return false;
584
+ }
585
+ }
586
+ async function waitForPort(port2, timeout = 5e3) {
587
+ const startTime = Date.now();
588
+ while (Date.now() - startTime < timeout) {
589
+ if (!isPortInUse(port2)) {
590
+ return true;
591
+ }
592
+ await new Promise((resolve3) => setTimeout(resolve3, 100));
593
+ }
594
+ return false;
595
+ }
596
+
597
+ // core/NoteManager.ts
598
+ import { existsSync, readFileSync, readdirSync as readdirSync2, writeFileSync } from "fs";
599
+ import { join as join2 } from "path";
600
+
601
+ // config/constants.ts
602
+ import { resolve } from "path";
603
+ var configManager = getConfigManager();
604
+ var config = configManager.getAll();
605
+ var {
606
+ author,
607
+ ignore_dirs,
608
+ menuItems,
609
+ port,
610
+ repoName,
611
+ sidebarShowNoteId,
612
+ socialLinks,
613
+ root_item
614
+ } = config;
615
+ var rootPath = configManager.getRootPath();
616
+ var TNOTES_BASE_DIR = resolve(rootPath, "..");
617
+ var TNOTES_CORE_DIR = resolve(TNOTES_BASE_DIR, "TNotes.core");
618
+ var EN_WORDS_DIR = resolve(TNOTES_BASE_DIR, "TNotes.en-words");
619
+ var ROOT_DIR_PATH = rootPath;
620
+ var ROOT_README_PATH = resolve(ROOT_DIR_PATH, "README.md");
621
+ var ROOT_CONFIG_PATH = resolve(ROOT_DIR_PATH, ".tnotes.json");
622
+ var NOTES_DIR_PATH = resolve(ROOT_DIR_PATH, "notes");
623
+ var VP_DIR_PATH = resolve(ROOT_DIR_PATH, ".vitepress");
624
+ var PUBLIC_PATH = resolve(ROOT_DIR_PATH, "public");
625
+ var GITHUB_DIR_PATH = resolve(ROOT_DIR_PATH, ".github");
626
+ var GITHUB_DEPLOY_YML_PATH = resolve(
627
+ GITHUB_DIR_PATH,
628
+ "workflows",
629
+ "deploy.yml"
630
+ );
631
+ var VP_SIDEBAR_PATH = resolve(ROOT_DIR_PATH, "sidebar.json");
632
+ var ROOT_PKG_PATH = resolve(ROOT_DIR_PATH, "package.json");
633
+ var VSCODE_SETTINGS_PATH = resolve(
634
+ ROOT_DIR_PATH,
635
+ ".vscode",
636
+ "settings.json"
637
+ );
638
+ var VSCODE_TASKS_PATH = resolve(ROOT_DIR_PATH, ".vscode", "tasks.json");
639
+ var EOL = "\n";
640
+ var CONSTANTS = {
641
+ // 端口配置
642
+ DEFAULT_PORT: 5173,
643
+ // 笔记索引配置(文件夹前缀的 4 位数字)
644
+ NOTE_INDEX_LENGTH: 4,
645
+ NOTE_INDEX_PATTERN: /^\d{4}\./,
646
+ NOTE_INDEX_PREFIX_PATTERN: /^\d{4}/,
647
+ // Git 配置
648
+ DEFAULT_BRANCH: "main",
649
+ // 缓存配置
650
+ CACHE_TTL: 5e3,
651
+ // 终端输出颜色
652
+ COLORS: {
653
+ RESET: "\x1B[0m",
654
+ BRIGHT: "\x1B[1m",
655
+ DIM: "\x1B[2m",
656
+ RED: "\x1B[31m",
657
+ GREEN: "\x1B[32m",
658
+ YELLOW: "\x1B[33m",
659
+ BLUE: "\x1B[34m",
660
+ MAGENTA: "\x1B[35m",
661
+ CYAN: "\x1B[36m"
662
+ },
663
+ // Emoji
664
+ EMOJI: {
665
+ SUCCESS: "\u2705",
666
+ ERROR: "\u274C",
667
+ WARNING: "\u26A0\uFE0F",
668
+ INFO: "\u2139\uFE0F",
669
+ PROGRESS: "\u23F3",
670
+ ROCKET: "\u{1F680}",
671
+ STOP: "\u{1F6D1}",
672
+ SPARKLES: "\u2728",
673
+ LINK: "\u{1F517}",
674
+ FILE: "\u{1F4C4}",
675
+ GIT: "\u{1F4E6}",
676
+ DEBUG: "\u{1F41B}"
677
+ }
678
+ };
679
+ var NOTES_PATH = NOTES_DIR_PATH;
680
+ var REPO_NOTES_URL = `https://github.com/${author}/${repoName}/tree/main/notes`;
681
+
682
+ // core/NoteManager.ts
683
+ var NoteManager = class _NoteManager {
684
+ static instance;
685
+ /** 笔记索引正则:4 位数字开头,后接小数点 */
686
+ static NOTE_INDEX_REGEX = /^(\d{4})\./;
687
+ constructor() {
688
+ }
689
+ /**
690
+ * 从文件夹名称或文本中提取笔记索引
691
+ *
692
+ * @param text - 要解析的文本(通常是文件夹名称)
693
+ * @returns 笔记索引(4 位数字字符串)或 null
694
+ *
695
+ * @example
696
+ * NoteManager.extractNoteIndex('0001. TNotes 简介') // '0001'
697
+ * NoteManager.extractNoteIndex('invalid-folder') // null
698
+ */
699
+ static extractNoteIndex(text) {
700
+ const match = text.match(_NoteManager.NOTE_INDEX_REGEX);
701
+ return match ? match[1] : null;
702
+ }
703
+ /**
704
+ * 输出无效笔记名称的警告日志
705
+ *
706
+ * @param name - 无效的笔记名称
707
+ */
708
+ static warnInvalidNoteIndex(name) {
709
+ logger.warn(`\u65E0\u6548\u7684\u7B14\u8BB0\u540D: ${name}`);
710
+ logger.warn("\u7B14\u8BB0\u540D\u5FC5\u987B\u4EE5 4 \u4E2A\u6570\u5B57\u5F00\u5934");
711
+ logger.warn("\u8303\u56F4\uFF1A0001-9999");
712
+ }
713
+ static getInstance() {
714
+ if (!_NoteManager.instance) {
715
+ _NoteManager.instance = new _NoteManager();
716
+ }
717
+ return _NoteManager.instance;
718
+ }
719
+ /**
720
+ * 获取 notes 目录下所有合法的笔记目录名(已排序)
721
+ * 合法条件:是目录、不以 . 开头、以 4 位数字 + . 开头
722
+ */
723
+ getNoteDirs() {
724
+ if (!existsSync(NOTES_PATH)) return [];
725
+ return readdirSync2(NOTES_PATH, { withFileTypes: true }).filter(
726
+ (entry) => entry.isDirectory() && !entry.name.startsWith(".") && _NoteManager.NOTE_INDEX_REGEX.test(entry.name)
727
+ ).map((entry) => entry.name).sort();
728
+ }
729
+ /**
730
+ * 根据目录名构建单条 NoteInfo
731
+ * @returns NoteInfo 或 undefined(README 不存在时)
732
+ */
733
+ buildNoteInfo(dirName) {
734
+ const notePath = join2(NOTES_PATH, dirName);
735
+ const readmePath = join2(notePath, "README.md");
736
+ const configPath = join2(notePath, ".tnotes.json");
737
+ if (!existsSync(readmePath)) {
738
+ logger.warn(`README not found in note: ${dirName}`);
739
+ return void 0;
740
+ }
741
+ let config2;
742
+ if (existsSync(configPath)) {
743
+ config2 = this.validateAndFixConfig(configPath) || void 0;
744
+ }
745
+ return {
746
+ index: _NoteManager.extractNoteIndex(dirName),
747
+ path: notePath,
748
+ dirName,
749
+ readmePath,
750
+ configPath,
751
+ config: config2
752
+ };
753
+ }
754
+ /**
755
+ * 扫描所有笔记并校验数据完整性
756
+ *
757
+ * @returns 笔记信息数组
758
+ */
759
+ scanNotes() {
760
+ const noteDirs = this.getNoteDirs();
761
+ if (noteDirs.length === 0) {
762
+ logger.warn(`${NOTES_PATH} \u672A\u68C0\u6D4B\u5230\u7B14\u8BB0\u76EE\u5F55`);
763
+ return [];
764
+ }
765
+ const notes = [];
766
+ for (const dirName of noteDirs) {
767
+ const note = this.buildNoteInfo(dirName);
768
+ if (note) notes.push(note);
769
+ }
770
+ this.validateNotes(notes);
771
+ return notes;
772
+ }
773
+ /**
774
+ * 校验笔记数据完整性
775
+ *
776
+ * - 检查 noteIndex 冲突 + config id 缺失/重复
777
+ * - 任一检查失败则终止进程
778
+ */
779
+ validateNotes(notes) {
780
+ const errors = [];
781
+ const L1 = " ".repeat(3);
782
+ const L2 = " ".repeat(6);
783
+ const indexMap = this.buildNoteIndexMap(notes.map((n) => n.dirName));
784
+ for (const [index, dirNames] of indexMap.entries()) {
785
+ if (dirNames.length > 1) {
786
+ errors.push(`\u26A0\uFE0F \u68C0\u6D4B\u5230\u91CD\u590D\u7684\u7B14\u8BB0\u7F16\u53F7\uFF1A`);
787
+ errors.push(`${L1}\u7D22\u5F15 ${index} \u88AB\u4EE5\u4E0B\u7B14\u8BB0\u91CD\u590D\u4F7F\u7528\uFF1A`);
788
+ dirNames.forEach((dirName) => errors.push(`${L2}- ${dirName}`));
789
+ }
790
+ }
791
+ const missingConfigId = [];
792
+ for (const note of notes) {
793
+ if (!note.config || !note.config.id) {
794
+ missingConfigId.push(note.dirName);
795
+ }
796
+ }
797
+ if (missingConfigId.length > 0) {
798
+ errors.push(`\u26A0\uFE0F \u68C0\u6D4B\u5230\u7B14\u8BB0\u914D\u7F6E ID \u7F3A\u5931\uFF1A`);
799
+ missingConfigId.forEach((dirName) => errors.push(`${L2}- ${dirName}`));
800
+ }
801
+ const configIdMap = /* @__PURE__ */ new Map();
802
+ for (const note of notes) {
803
+ if (note.config?.id) {
804
+ if (!configIdMap.has(note.config.id))
805
+ configIdMap.set(note.config.id, []);
806
+ configIdMap.get(note.config.id).push(note.dirName);
807
+ }
808
+ }
809
+ for (const [configId, dirNames] of configIdMap.entries()) {
810
+ if (dirNames.length > 1) {
811
+ errors.push(`\u26A0\uFE0F \u68C0\u6D4B\u5230\u91CD\u590D\u7684\u7B14\u8BB0\u914D\u7F6E ID\uFF1A`);
812
+ errors.push(`${L1}\u914D\u7F6E ID ${configId} \u88AB\u4EE5\u4E0B\u7B14\u8BB0\u91CD\u590D\u4F7F\u7528\uFF1A`);
813
+ dirNames.forEach((dirName) => errors.push(`${L2}- ${dirName}`));
814
+ }
815
+ }
816
+ if (errors.length > 0) {
817
+ for (const line of errors) {
818
+ logger.error(line);
819
+ }
820
+ logger.error("\n\n\u8BF7\u4FEE\u590D\u4E0A\u8FF0\u95EE\u9898\u540E\u91CD\u65B0\u542F\u52A8\u670D\u52A1\u3002\n\n");
821
+ process.exit(1);
822
+ }
823
+ }
824
+ /**
825
+ * 按 4 位数字编号对目录名分组
826
+ * @param dirNames - 目录名数组
827
+ * @returns 编号 -> 目录名数组 的映射
828
+ */
829
+ buildNoteIndexMap(dirNames) {
830
+ const indexMap = /* @__PURE__ */ new Map();
831
+ for (const name of dirNames) {
832
+ const index = _NoteManager.extractNoteIndex(name);
833
+ if (!indexMap.has(index)) indexMap.set(index, []);
834
+ indexMap.get(index).push(name);
835
+ }
836
+ return indexMap;
837
+ }
838
+ /** 配置字段顺序 */
839
+ static FIELD_ORDER = [
840
+ "bilibili",
841
+ "tnotes",
842
+ "yuque",
843
+ "done",
844
+ "category",
845
+ "enableDiscussions",
846
+ "description",
847
+ "id",
848
+ "created_at",
849
+ "updated_at"
850
+ ];
851
+ /** 默认配置字段 */
852
+ static DEFAULT_CONFIG_FIELDS = {
853
+ bilibili: [],
854
+ tnotes: [],
855
+ yuque: [],
856
+ done: false,
857
+ enableDiscussions: false,
858
+ description: ""
859
+ };
860
+ /** 必需字段(不能缺失) */
861
+ static REQUIRED_FIELDS = ["id"];
862
+ /**
863
+ * 验证并修复配置文件
864
+ * @param configPath - 配置文件路径
865
+ * @returns 修复后的配置对象,失败时返回 null
866
+ */
867
+ validateAndFixConfig(configPath) {
868
+ const configContent = readFileSync(configPath, "utf-8");
869
+ let config2;
870
+ try {
871
+ config2 = JSON.parse(configContent);
872
+ } catch (error) {
873
+ logger.error(`\u914D\u7F6E\u6587\u4EF6 JSON \u89E3\u6790\u5931\u8D25: ${configPath}`, error);
874
+ return null;
875
+ }
876
+ let needsUpdate = false;
877
+ for (const field of _NoteManager.REQUIRED_FIELDS) {
878
+ if (!config2[field]) {
879
+ return null;
880
+ }
881
+ }
882
+ for (const [key, defaultValue] of Object.entries(
883
+ _NoteManager.DEFAULT_CONFIG_FIELDS
884
+ )) {
885
+ if (!(key in config2)) {
886
+ ;
887
+ config2[key] = defaultValue;
888
+ needsUpdate = true;
889
+ logger.info(`\u8865\u5145\u7F3A\u5931\u5B57\u6BB5 "${key}": ${configPath}`);
890
+ }
891
+ }
892
+ const now = Date.now();
893
+ if (!config2.created_at) {
894
+ config2.created_at = now;
895
+ needsUpdate = true;
896
+ logger.info(
897
+ `\u68C0\u6D4B\u5230 ${configPath} \u7F3A\u5931 created_at \u5B57\u6BB5\uFF0C\u8BF7\u6267\u884C tn:fix-timestamps \u6821\u51C6\u4E3A\u7B14\u8BB0\u9996\u6B21 git commit \u7684\u65F6\u95F4\uFF09`
898
+ );
899
+ }
900
+ if (!config2.updated_at) {
901
+ config2.updated_at = now;
902
+ needsUpdate = true;
903
+ logger.info(
904
+ `\u68C0\u6D4B\u5230 ${configPath} \u7F3A\u5931 updated_at \u5B57\u6BB5\uFF0C\u8BF7\u6267\u884C tn:fix-timestamps \u6821\u51C6\u4E3A\u7B14\u8BB0\u6700\u540E\u4E00\u6B21 git commit \u7684\u65F6\u95F4\uFF09`
905
+ );
906
+ }
907
+ const sortedConfig = this.sortConfigKeys(config2);
908
+ if (needsUpdate) {
909
+ this.writeNoteConfig(configPath, sortedConfig);
910
+ logger.info(`\u914D\u7F6E\u6587\u4EF6\u5DF2\u4FEE\u590D: ${configPath}`);
911
+ }
912
+ return sortedConfig;
913
+ }
914
+ /**
915
+ * 按指定顺序排序配置对象的键
916
+ */
917
+ sortConfigKeys(config2) {
918
+ const configRecord = config2;
919
+ const sorted = {};
920
+ for (const key of _NoteManager.FIELD_ORDER) {
921
+ if (key in config2) {
922
+ sorted[key] = configRecord[key];
923
+ }
924
+ }
925
+ for (const key of Object.keys(config2)) {
926
+ if (!(key in sorted)) {
927
+ sorted[key] = configRecord[key];
928
+ }
929
+ }
930
+ return sorted;
931
+ }
932
+ /**
933
+ * 序列化 NoteConfig 为格式化的 JSON 字符串
934
+ * 保持字段顺序,使用 2 空格缩进,末尾含换行符
935
+ */
936
+ serializeNoteConfig(config2) {
937
+ const sorted = this.sortConfigKeys(config2);
938
+ return JSON.stringify(sorted, null, 2) + "\n";
939
+ }
940
+ /**
941
+ * 统一写入笔记配置文件
942
+ * @param configPath - 配置文件路径
943
+ * @param config - 笔记配置
944
+ */
945
+ writeNoteConfig(configPath, config2) {
946
+ writeFileSync(configPath, this.serializeNoteConfig(config2), "utf-8");
947
+ }
948
+ /**
949
+ * 验证笔记配置对象的结构合法性
950
+ * @param config - 笔记配置
951
+ * @returns 是否有效
952
+ */
953
+ validateConfig(config2) {
954
+ if (!config2.id) {
955
+ logger.error("Note config missing id");
956
+ return false;
957
+ }
958
+ if (!Array.isArray(config2.bilibili)) {
959
+ logger.error(`Invalid bilibili config in note: ${config2.id}`);
960
+ return false;
961
+ }
962
+ if (!Array.isArray(config2.tnotes)) {
963
+ logger.error(`Invalid tnotes config in note: ${config2.id}`);
964
+ return false;
965
+ }
966
+ if (!Array.isArray(config2.yuque)) {
967
+ logger.error(`Invalid yuque config in note: ${config2.id}`);
968
+ return false;
969
+ }
970
+ if (typeof config2.done !== "boolean") {
971
+ logger.error(`Invalid done status in note: ${config2.id}`);
972
+ return false;
973
+ }
974
+ if (typeof config2.enableDiscussions !== "boolean") {
975
+ logger.error(`Invalid enableDiscussions status in note: ${config2.id}`);
976
+ return false;
977
+ }
978
+ return true;
979
+ }
980
+ /**
981
+ * 更新笔记配置
982
+ * @param noteInfo - 笔记信息
983
+ * @param config - 新的配置
984
+ */
985
+ updateNoteConfig(noteInfo, config2) {
986
+ if (!this.validateConfig(config2)) {
987
+ throw new Error(`Invalid config for note: ${noteInfo.dirName}`);
988
+ }
989
+ config2.updated_at = Date.now();
990
+ this.writeNoteConfig(noteInfo.configPath, config2);
991
+ logger.info(`Updated config for note: ${noteInfo.dirName}`);
992
+ }
993
+ /**
994
+ * 获取笔记信息(通过索引)- 直接查找不扫描所有笔记
995
+ * @param noteIndex - 笔记索引
996
+ * @returns 笔记信息,未找到时返回 undefined
997
+ */
998
+ getNoteByIndex(noteIndex) {
999
+ const noteDirs = this.getNoteDirs();
1000
+ for (const dirName of noteDirs) {
1001
+ if (_NoteManager.extractNoteIndex(dirName) === noteIndex) {
1002
+ return this.buildNoteInfo(dirName);
1003
+ }
1004
+ }
1005
+ return void 0;
1006
+ }
1007
+ };
1008
+
1009
+ // utils/readmeHelpers.ts
1010
+ var NOTE_LINE_REGEX = /^( *)- \[.\] \[(\d{4}\. .+?)\]/;
1011
+ function parseNoteLine(line) {
1012
+ const noteMatch = line.match(NOTE_LINE_REGEX);
1013
+ if (!noteMatch) {
1014
+ return {
1015
+ isMatch: false,
1016
+ noteIndex: null
1017
+ };
1018
+ }
1019
+ const [, , text] = noteMatch;
1020
+ const noteIndex = NoteManager.extractNoteIndex(text);
1021
+ return {
1022
+ isMatch: true,
1023
+ noteIndex
1024
+ };
1025
+ }
1026
+ function buildNoteLink(note, repoOwner, repoName2) {
1027
+ const encodedDirName = encodeURIComponent(note.dirName);
1028
+ return `https://github.com/${repoOwner}/${repoName2}/tree/main/notes/${encodedDirName}/README.md`;
1029
+ }
1030
+ function updateNoteStatus(note) {
1031
+ let status = " ";
1032
+ let deprecatedMark = "";
1033
+ if (note.config) {
1034
+ if (note.config.done) {
1035
+ status = "x";
1036
+ }
1037
+ }
1038
+ return { status, deprecatedMark };
1039
+ }
1040
+ function buildNoteLineMarkdown(note, repoOwner, repoName2) {
1041
+ const url = buildNoteLink(note, repoOwner, repoName2);
1042
+ const { status, deprecatedMark } = updateNoteStatus(note);
1043
+ return `- [${status}] [${note.dirName}](${url})${deprecatedMark}`;
1044
+ }
1045
+ function isNoteLine(line) {
1046
+ return NOTE_LINE_REGEX.test(line);
1047
+ }
1048
+ function mergeConsecutiveEmptyLines(lines) {
1049
+ const result = [];
1050
+ let previousLineIsEmpty = false;
1051
+ for (const line of lines) {
1052
+ const isCurrentLineEmpty = line === "";
1053
+ if (isCurrentLineEmpty) {
1054
+ if (!previousLineIsEmpty) {
1055
+ result.push(line);
1056
+ previousLineIsEmpty = true;
1057
+ }
1058
+ } else {
1059
+ result.push(line);
1060
+ previousLineIsEmpty = false;
1061
+ }
1062
+ }
1063
+ return result;
1064
+ }
1065
+ function removeEmptyLinesBetweenNotes(lines) {
1066
+ const result = [];
1067
+ for (let i = 0; i < lines.length; i++) {
1068
+ const currentLine = lines[i];
1069
+ const prevLine = i > 0 ? lines[i - 1] : null;
1070
+ const nextLine = i < lines.length - 1 ? lines[i + 1] : null;
1071
+ if (currentLine === "" && prevLine && nextLine) {
1072
+ const isPrevLineNote = isNoteLine(prevLine);
1073
+ const isNextLineNote = isNoteLine(nextLine);
1074
+ if (isPrevLineNote && isNextLineNote) {
1075
+ continue;
1076
+ }
1077
+ }
1078
+ result.push(currentLine);
1079
+ }
1080
+ return result;
1081
+ }
1082
+ function processEmptyLines(lines) {
1083
+ const stepOne = mergeConsecutiveEmptyLines(lines);
1084
+ const stepTwo = removeEmptyLinesBetweenNotes(stepOne);
1085
+ return stepTwo;
1086
+ }
1087
+
1088
+ // utils/runCommand.ts
1089
+ import { exec } from "child_process";
1090
+ async function runCommand(command, dir) {
1091
+ return new Promise((resolve3, reject) => {
1092
+ exec(command, { cwd: dir }, (error, stdout, stderr) => {
1093
+ if (error) {
1094
+ console.error(`\u5904\u7406 ${dir} \u65F6\u51FA\u9519\uFF1A${stderr}`);
1095
+ reject(error);
1096
+ } else {
1097
+ resolve3(stdout.trim());
1098
+ }
1099
+ });
1100
+ });
1101
+ }
1102
+
1103
+ // utils/syncRepo.ts
1104
+ async function pushAllRepos(options) {
1105
+ const {
1106
+ parallel = true,
1107
+ continueOnError = true,
1108
+ force = false
1109
+ } = options || {};
1110
+ const targetDirs = getTargetDirs(TNOTES_BASE_DIR, "TNotes.", [EN_WORDS_DIR, TNOTES_CORE_DIR]);
1111
+ logger.info(`\u6B63\u5728\u63A8\u9001 ${targetDirs.length} \u4E2A\u4ED3\u5E93...`);
1112
+ if (force) {
1113
+ logger.warn("\u4F7F\u7528\u5F3A\u5236\u63A8\u9001\u6A21\u5F0F");
1114
+ }
1115
+ const results = [];
1116
+ const pushCmd = force ? "pnpm tn:push --force" : "pnpm tn:push";
1117
+ if (parallel) {
1118
+ const promises = targetDirs.map(async (dir, index) => {
1119
+ try {
1120
+ await runCommand(pushCmd, dir);
1121
+ process.stdout.write(`\r \u8FDB\u5EA6: ~${index + 1}/${targetDirs.length}`);
1122
+ return { dir, success: true };
1123
+ } catch (error) {
1124
+ const errorMessage = error instanceof Error ? error.message : String(error);
1125
+ return { dir, success: false, error: errorMessage };
1126
+ }
1127
+ });
1128
+ results.push(...await Promise.all(promises));
1129
+ console.log();
1130
+ } else {
1131
+ for (let i = 0; i < targetDirs.length; i++) {
1132
+ const dir = targetDirs[i];
1133
+ try {
1134
+ await runCommand(pushCmd, dir);
1135
+ process.stdout.write(`\r \u8FDB\u5EA6: ${i + 1}/${targetDirs.length}`);
1136
+ results.push({ dir, success: true });
1137
+ } catch (error) {
1138
+ process.stdout.write(`\r \u8FDB\u5EA6: ${i + 1}/${targetDirs.length}`);
1139
+ const errorMessage = error instanceof Error ? error.message : String(error);
1140
+ results.push({ dir, success: false, error: errorMessage });
1141
+ if (!continueOnError) {
1142
+ console.log();
1143
+ throw error;
1144
+ }
1145
+ }
1146
+ }
1147
+ console.log();
1148
+ }
1149
+ const successCount = results.filter((r) => r.success).length;
1150
+ const failCount = results.length - successCount;
1151
+ if (failCount === 0) {
1152
+ logger.success(`\u63A8\u9001\u5B8C\u6210: ${successCount}/${results.length} \u4E2A\u4ED3\u5E93\u6210\u529F`);
1153
+ } else {
1154
+ logger.warn(
1155
+ `\u63A8\u9001\u5B8C\u6210: ${successCount} \u6210\u529F, ${failCount} \u5931\u8D25 (\u5171 ${results.length} \u4E2A)`
1156
+ );
1157
+ console.log("\n\u5931\u8D25\u7684\u4ED3\u5E93:");
1158
+ results.filter((r) => !r.success).forEach((r, index) => {
1159
+ const repoName2 = r.dir.split("\\").pop() || r.dir;
1160
+ console.log(` ${index + 1}. ${repoName2}`);
1161
+ console.log(` \u9519\u8BEF: ${r.error}`);
1162
+ });
1163
+ }
1164
+ }
1165
+ async function pullAllRepos(options) {
1166
+ const { parallel = true, continueOnError = true } = options || {};
1167
+ const targetDirs = getTargetDirs(TNOTES_BASE_DIR, "TNotes.", [EN_WORDS_DIR, TNOTES_CORE_DIR]);
1168
+ logger.info(`\u6B63\u5728\u62C9\u53D6 ${targetDirs.length} \u4E2A\u4ED3\u5E93...`);
1169
+ const results = [];
1170
+ if (parallel) {
1171
+ const promises = targetDirs.map(async (dir, index) => {
1172
+ try {
1173
+ await runCommand("pnpm tn:pull", dir);
1174
+ process.stdout.write(`\r \u8FDB\u5EA6: ~${index + 1}/${targetDirs.length}`);
1175
+ return { dir, success: true };
1176
+ } catch (error) {
1177
+ const errorMessage = error instanceof Error ? error.message : String(error);
1178
+ return { dir, success: false, error: errorMessage };
1179
+ }
1180
+ });
1181
+ results.push(...await Promise.all(promises));
1182
+ console.log();
1183
+ } else {
1184
+ for (let i = 0; i < targetDirs.length; i++) {
1185
+ const dir = targetDirs[i];
1186
+ try {
1187
+ await runCommand("pnpm tn:pull", dir);
1188
+ process.stdout.write(`\r \u8FDB\u5EA6: ${i + 1}/${targetDirs.length}`);
1189
+ results.push({ dir, success: true });
1190
+ } catch (error) {
1191
+ process.stdout.write(`\r \u8FDB\u5EA6: ${i + 1}/${targetDirs.length}`);
1192
+ const errorMessage = error instanceof Error ? error.message : String(error);
1193
+ results.push({ dir, success: false, error: errorMessage });
1194
+ if (!continueOnError) {
1195
+ console.log();
1196
+ throw error;
1197
+ }
1198
+ }
1199
+ }
1200
+ console.log();
1201
+ }
1202
+ const successCount = results.filter((r) => r.success).length;
1203
+ const failCount = results.length - successCount;
1204
+ if (failCount === 0) {
1205
+ logger.success(`\u62C9\u53D6\u5B8C\u6210: ${successCount}/${results.length} \u4E2A\u4ED3\u5E93\u6210\u529F`);
1206
+ } else {
1207
+ logger.warn(
1208
+ `\u62C9\u53D6\u5B8C\u6210: ${successCount} \u6210\u529F, ${failCount} \u5931\u8D25 (\u5171 ${results.length} \u4E2A)`
1209
+ );
1210
+ console.log("\n\u5931\u8D25\u7684\u4ED3\u5E93:");
1211
+ results.filter((r) => !r.success).forEach((r, index) => {
1212
+ const repoName2 = r.dir.split("\\").pop() || r.dir;
1213
+ console.log(` ${index + 1}. ${repoName2}`);
1214
+ console.log(` \u9519\u8BEF: ${r.error}`);
1215
+ });
1216
+ }
1217
+ }
1218
+ async function syncAllRepos(options) {
1219
+ const { parallel = true, continueOnError = true } = options || {};
1220
+ const targetDirs = getTargetDirs(TNOTES_BASE_DIR, "TNotes.", [EN_WORDS_DIR, TNOTES_CORE_DIR]);
1221
+ logger.info(`\u6B63\u5728\u540C\u6B65 ${targetDirs.length} \u4E2A\u4ED3\u5E93...`);
1222
+ const results = [];
1223
+ if (parallel) {
1224
+ const promises = targetDirs.map(async (dir, index) => {
1225
+ try {
1226
+ await runCommand("pnpm tn:sync", dir);
1227
+ process.stdout.write(`\r \u8FDB\u5EA6: ~${index + 1}/${targetDirs.length}`);
1228
+ return { dir, success: true };
1229
+ } catch (error) {
1230
+ const errorMessage = error instanceof Error ? error.message : String(error);
1231
+ return { dir, success: false, error: errorMessage };
1232
+ }
1233
+ });
1234
+ results.push(...await Promise.all(promises));
1235
+ } else {
1236
+ for (let i = 0; i < targetDirs.length; i++) {
1237
+ const dir = targetDirs[i];
1238
+ try {
1239
+ await runCommand("pnpm tn:sync", dir);
1240
+ process.stdout.write(`\r \u8FDB\u5EA6: ${i + 1}/${targetDirs.length}`);
1241
+ results.push({ dir, success: true });
1242
+ } catch (error) {
1243
+ process.stdout.write(`\r \u8FDB\u5EA6: ${i + 1}/${targetDirs.length}`);
1244
+ const errorMessage = error instanceof Error ? error.message : String(error);
1245
+ results.push({ dir, success: false, error: errorMessage });
1246
+ if (!continueOnError) {
1247
+ throw error;
1248
+ }
1249
+ }
1250
+ }
1251
+ }
1252
+ console.log();
1253
+ const successCount = results.filter((r) => r.success).length;
1254
+ const failCount = results.length - successCount;
1255
+ if (failCount === 0) {
1256
+ logger.success(`\u540C\u6B65\u5B8C\u6210: ${successCount}/${results.length} \u4E2A\u4ED3\u5E93\u6210\u529F`);
1257
+ } else {
1258
+ logger.warn(
1259
+ `\u540C\u6B65\u5B8C\u6210: ${successCount} \u6210\u529F, ${failCount} \u5931\u8D25 (\u5171 ${results.length} \u4E2A)`
1260
+ );
1261
+ console.log("\n\u5931\u8D25\u7684\u4ED3\u5E93:");
1262
+ results.filter((r) => !r.success).forEach((r, index) => {
1263
+ const repoName2 = r.dir.split("\\").pop() || r.dir;
1264
+ console.log(` ${index + 1}. ${repoName2}`);
1265
+ console.log(` \u9519\u8BEF: ${r.error}`);
1266
+ });
1267
+ }
1268
+ }
1269
+
1270
+ // utils/validators.ts
1271
+ var INVALID_FILENAME_CHARS = /[<>:"/\\|?*\x00-\x1F]/;
1272
+ var WINDOWS_RESERVED_NAMES = /* @__PURE__ */ new Set([
1273
+ "CON",
1274
+ "PRN",
1275
+ "AUX",
1276
+ "NUL",
1277
+ "COM1",
1278
+ "COM2",
1279
+ "COM3",
1280
+ "COM4",
1281
+ "COM5",
1282
+ "COM6",
1283
+ "COM7",
1284
+ "COM8",
1285
+ "COM9",
1286
+ "LPT1",
1287
+ "LPT2",
1288
+ "LPT3",
1289
+ "LPT4",
1290
+ "LPT5",
1291
+ "LPT6",
1292
+ "LPT7",
1293
+ "LPT8",
1294
+ "LPT9"
1295
+ ]);
1296
+ function validateNoteTitle(title) {
1297
+ if (!title || title.trim().length === 0) {
1298
+ return { valid: false, error: "\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A" };
1299
+ }
1300
+ const trimmedTitle = title.trim();
1301
+ if (trimmedTitle.length > 200) {
1302
+ return { valid: false, error: "\u6807\u9898\u8FC7\u957F(\u6700\u591A200\u4E2A\u5B57\u7B26)" };
1303
+ }
1304
+ if (INVALID_FILENAME_CHARS.test(trimmedTitle)) {
1305
+ return {
1306
+ valid: false,
1307
+ error: '\u6807\u9898\u5305\u542B\u975E\u6CD5\u5B57\u7B26(\u4E0D\u5141\u8BB8: < > : " / \\ | ? *)'
1308
+ };
1309
+ }
1310
+ if (/^[.\s]|[.\s]$/.test(trimmedTitle)) {
1311
+ return {
1312
+ valid: false,
1313
+ error: "\u6807\u9898\u4E0D\u80FD\u4EE5\u70B9\u6216\u7A7A\u683C\u5F00\u5934/\u7ED3\u5C3E"
1314
+ };
1315
+ }
1316
+ const upperTitle = trimmedTitle.toUpperCase();
1317
+ if (WINDOWS_RESERVED_NAMES.has(upperTitle)) {
1318
+ return {
1319
+ valid: false,
1320
+ error: `"${trimmedTitle}" \u662F Windows \u7CFB\u7EDF\u4FDD\u7559\u540D\u79F0`
1321
+ };
1322
+ }
1323
+ const baseName = trimmedTitle.split(".")[0].toUpperCase();
1324
+ if (WINDOWS_RESERVED_NAMES.has(baseName)) {
1325
+ return {
1326
+ valid: false,
1327
+ error: `"${trimmedTitle}" \u5305\u542B Windows \u7CFB\u7EDF\u4FDD\u7559\u540D\u79F0`
1328
+ };
1329
+ }
1330
+ return { valid: true };
1331
+ }
1332
+
1333
+ // core/NoteIndexCache.ts
1334
+ import { join as join3 } from "path";
1335
+ var NoteIndexCache = class _NoteIndexCache {
1336
+ static instance = null;
1337
+ /** noteIndex -> NoteIndexItem 的映射 */
1338
+ byNoteIndex = /* @__PURE__ */ new Map();
1339
+ /** configId (UUID) -> noteIndex 的映射,用于快速反向查询 */
1340
+ byConfigId = /* @__PURE__ */ new Map();
1341
+ /** 是否已完成初始化 */
1342
+ _initialized = false;
1343
+ constructor() {
1344
+ }
1345
+ /**
1346
+ * 获取单例实例
1347
+ */
1348
+ static getInstance() {
1349
+ if (!_NoteIndexCache.instance) {
1350
+ _NoteIndexCache.instance = new _NoteIndexCache();
1351
+ }
1352
+ return _NoteIndexCache.instance;
1353
+ }
1354
+ /**
1355
+ * 初始化索引缓存
1356
+ * @param notes - 扫描得到的笔记列表(已由 NoteManager.scanNotes 完成重复检测)
1357
+ */
1358
+ initialize(notes) {
1359
+ this.byNoteIndex.clear();
1360
+ this.byConfigId.clear();
1361
+ for (const note of notes) {
1362
+ const item = {
1363
+ noteIndex: note.index,
1364
+ folderName: note.dirName,
1365
+ noteConfig: note.config
1366
+ };
1367
+ this.byNoteIndex.set(note.index, item);
1368
+ this.byConfigId.set(note.config.id, note.index);
1369
+ }
1370
+ this._initialized = true;
1371
+ }
1372
+ /**
1373
+ * 是否已完成初始化
1374
+ */
1375
+ isInitialized() {
1376
+ return this._initialized;
1377
+ }
1378
+ /**
1379
+ * 从缓存构建 NoteInfo 列表(纯内存,零 I/O)
1380
+ * @returns 笔记信息数组
1381
+ */
1382
+ toNoteInfoList() {
1383
+ const result = [];
1384
+ for (const item of this.byNoteIndex.values()) {
1385
+ const notePath = join3(NOTES_PATH, item.folderName);
1386
+ result.push({
1387
+ index: item.noteIndex,
1388
+ path: notePath,
1389
+ dirName: item.folderName,
1390
+ readmePath: join3(notePath, "README.md"),
1391
+ configPath: join3(notePath, ".tnotes.json"),
1392
+ config: item.noteConfig
1393
+ });
1394
+ }
1395
+ return result;
1396
+ }
1397
+ /**
1398
+ * 根据 noteIndex 获取索引项
1399
+ */
1400
+ getByNoteIndex(noteIndex) {
1401
+ return this.byNoteIndex.get(noteIndex);
1402
+ }
1403
+ /**
1404
+ * 根据 configId (UUID) 获取索引项
1405
+ */
1406
+ getByConfigId(configId) {
1407
+ const noteIndex = this.byConfigId.get(configId);
1408
+ return noteIndex ? this.byNoteIndex.get(noteIndex) : void 0;
1409
+ }
1410
+ /**
1411
+ * 检查 noteIndex 是否存在
1412
+ */
1413
+ has(noteIndex) {
1414
+ return this.byNoteIndex.has(noteIndex);
1415
+ }
1416
+ /**
1417
+ * 更新笔记配置
1418
+ * @param noteIndex - 笔记索引
1419
+ * @param configUpdates - 要更新的配置字段
1420
+ */
1421
+ updateConfig(noteIndex, configUpdates) {
1422
+ const item = this.byNoteIndex.get(noteIndex);
1423
+ if (!item) {
1424
+ logger.warn(`\u5C1D\u8BD5\u66F4\u65B0\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0: ${noteIndex}`);
1425
+ return;
1426
+ }
1427
+ Object.assign(item.noteConfig, configUpdates);
1428
+ item.noteConfig.updated_at = Date.now();
1429
+ logger.debug(`\u66F4\u65B0\u7B14\u8BB0\u914D\u7F6E: ${noteIndex}`, configUpdates);
1430
+ }
1431
+ /**
1432
+ * 删除笔记
1433
+ * @param noteIndex - 笔记索引
1434
+ */
1435
+ delete(noteIndex) {
1436
+ const item = this.byNoteIndex.get(noteIndex);
1437
+ if (!item) {
1438
+ logger.warn(`\u5C1D\u8BD5\u5220\u9664\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0: ${noteIndex}`);
1439
+ return;
1440
+ }
1441
+ this.byNoteIndex.delete(noteIndex);
1442
+ this.byConfigId.delete(item.noteConfig.id);
1443
+ logger.info(`\u5220\u9664\u7B14\u8BB0\u7D22\u5F15: ${noteIndex}`);
1444
+ }
1445
+ /**
1446
+ * 添加新笔记
1447
+ * @param note - 笔记信息
1448
+ */
1449
+ add(note) {
1450
+ const item = {
1451
+ noteIndex: note.index,
1452
+ folderName: note.dirName,
1453
+ noteConfig: note.config
1454
+ };
1455
+ this.byNoteIndex.set(note.index, item);
1456
+ this.byConfigId.set(note.config.id, note.index);
1457
+ logger.info(`\u6DFB\u52A0\u7B14\u8BB0\u7D22\u5F15: ${note.index}`);
1458
+ }
1459
+ /**
1460
+ * 更新笔记的文件夹名称(标题变更时)
1461
+ * @param noteIndex - 笔记索引
1462
+ * @param newFolderName - 新的文件夹名称
1463
+ */
1464
+ updateFolderName(noteIndex, newFolderName) {
1465
+ const item = this.byNoteIndex.get(noteIndex);
1466
+ if (!item) {
1467
+ logger.warn(`\u5C1D\u8BD5\u66F4\u65B0\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0: ${noteIndex}`);
1468
+ return;
1469
+ }
1470
+ item.folderName = newFolderName;
1471
+ logger.debug(`\u66F4\u65B0\u7B14\u8BB0\u6587\u4EF6\u5939\u540D\u79F0: ${noteIndex} -> ${newFolderName}`);
1472
+ }
1473
+ };
1474
+
1475
+ // core/ReadmeGenerator.ts
1476
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
1477
+
1478
+ // core/TocGenerator.ts
1479
+ var BILIBILI_VIDEO_BASE_URL = "https://www.bilibili.com/video/";
1480
+ var TNOTES_YUQUE_BASE_URL = "https://www.yuque.com/tdahuyou/tnotes.yuque/";
1481
+ var NOTES_TOC_START_TAG = "<!-- region:toc -->";
1482
+ var NOTES_TOC_END_TAG = "<!-- endregion:toc -->";
1483
+ var TocGenerator = class {
1484
+ /**
1485
+ * 更新笔记目录
1486
+ * @param noteIndex - 笔记索引
1487
+ * @param lines - 笔记内容行数组
1488
+ * @param noteConfig - 笔记配置
1489
+ * @param repoName - 仓库名称
1490
+ */
1491
+ updateNoteToc(noteIndex, lines, noteConfig, repoName2) {
1492
+ let startLineIdx = -1, endLineIdx = -1;
1493
+ lines.forEach((line, idx) => {
1494
+ if (line.startsWith(NOTES_TOC_START_TAG)) startLineIdx = idx;
1495
+ if (line.startsWith(NOTES_TOC_END_TAG)) endLineIdx = idx;
1496
+ });
1497
+ if (startLineIdx === -1 || endLineIdx === -1) return;
1498
+ const titles = [];
1499
+ const numberedHeaders = ["## ", "### "];
1500
+ const unnumberedHeaders = ["#### ", "##### ", "###### "];
1501
+ const addNumberToTitle = createAddNumberToTitle();
1502
+ let inCodeBlock = false;
1503
+ let inHtmlComment = false;
1504
+ for (let i = 0; i < lines.length; i++) {
1505
+ const line = lines[i];
1506
+ if (line.trim().startsWith("```") || line.trim().startsWith("~~~")) {
1507
+ inCodeBlock = !inCodeBlock;
1508
+ continue;
1509
+ }
1510
+ if (line.trim().startsWith("<!--")) {
1511
+ inHtmlComment = true;
1512
+ }
1513
+ if (line.trim().includes("-->")) {
1514
+ inHtmlComment = false;
1515
+ continue;
1516
+ }
1517
+ if (inCodeBlock || inHtmlComment) {
1518
+ continue;
1519
+ }
1520
+ const isNumberedHeader = numberedHeaders.some(
1521
+ (header) => line.startsWith(header)
1522
+ );
1523
+ const isUnnumberedHeader = unnumberedHeaders.some(
1524
+ (header) => line.startsWith(header)
1525
+ );
1526
+ if (isNumberedHeader) {
1527
+ const [numberedTitle] = addNumberToTitle(line);
1528
+ titles.push(numberedTitle);
1529
+ lines[i] = numberedTitle;
1530
+ } else if (isUnnumberedHeader) {
1531
+ const match = line.match(/^(\#+)\s*(\d+(\.\d+)*\.\s*)?(.*)/);
1532
+ if (match) {
1533
+ const headerSymbol = match[1];
1534
+ const plainTitle = match[4];
1535
+ const cleanTitle = `${headerSymbol} ${plainTitle}`;
1536
+ titles.push(cleanTitle);
1537
+ lines[i] = cleanTitle;
1538
+ } else {
1539
+ titles.push(line);
1540
+ }
1541
+ }
1542
+ }
1543
+ const toc = generateToc(titles);
1544
+ const bilibiliTOCItems = [];
1545
+ const tnotesTOCItems = [];
1546
+ const yuqueTOCItems = [];
1547
+ if (noteConfig) {
1548
+ if (noteConfig.bilibili.length > 0) {
1549
+ noteConfig.bilibili.forEach((bvid, i) => {
1550
+ bilibiliTOCItems.push(
1551
+ ` - [bilibili.${repoName2}.${noteIndex}.${i + 1}](${BILIBILI_VIDEO_BASE_URL + bvid})`
1552
+ );
1553
+ });
1554
+ }
1555
+ if (noteConfig.tnotes && noteConfig.tnotes.length > 0) {
1556
+ tnotesTOCItems.push(
1557
+ `- [\u{1F4D2} TNotes\uFF08\u76F8\u5173\u77E5\u8BC6\u5E93\uFF09](https://tnotesjs.github.io/TNotes/)`
1558
+ );
1559
+ noteConfig.tnotes.forEach((repoName3) => {
1560
+ tnotesTOCItems.push(
1561
+ ` - [TNotes.${repoName3}](https://tnotesjs.github.io/TNotes.${repoName3}/)`
1562
+ );
1563
+ });
1564
+ }
1565
+ if (noteConfig.yuque.length > 0) {
1566
+ noteConfig.yuque.forEach((slug, i) => {
1567
+ yuqueTOCItems.push(
1568
+ ` - [TNotes.yuque.${repoName2.replace(
1569
+ "TNotes.",
1570
+ ""
1571
+ )}.${noteIndex}](${TNOTES_YUQUE_BASE_URL + slug})`
1572
+ );
1573
+ });
1574
+ }
1575
+ }
1576
+ const insertTocItems = [];
1577
+ const hasExternalResources = bilibiliTOCItems.length > 0 || tnotesTOCItems.length > 0 || yuqueTOCItems.length > 0;
1578
+ if (hasExternalResources) {
1579
+ insertTocItems.push("::: details \u{1F4DA} \u76F8\u5173\u8D44\u6E90", "");
1580
+ if (bilibiliTOCItems.length > 0) {
1581
+ insertTocItems.push(
1582
+ `- [\u{1F4FA} bilibili\uFF08\u7B14\u8BB0\u89C6\u9891\u8D44\u6E90\uFF09](https://space.bilibili.com/407241004)`,
1583
+ ...bilibiliTOCItems
1584
+ );
1585
+ }
1586
+ if (tnotesTOCItems.length > 0) {
1587
+ insertTocItems.push(...tnotesTOCItems);
1588
+ }
1589
+ if (yuqueTOCItems.length > 0) {
1590
+ insertTocItems.push(
1591
+ `- [\u{1F4C2} TNotes.yuque\uFF08\u7B14\u8BB0\u9644\u4EF6\u8D44\u6E90\uFF09](${TNOTES_YUQUE_BASE_URL})`,
1592
+ ...yuqueTOCItems
1593
+ );
1594
+ }
1595
+ insertTocItems.push("", ":::", "");
1596
+ }
1597
+ lines.splice(
1598
+ startLineIdx + 1,
1599
+ endLineIdx - startLineIdx - 1,
1600
+ "",
1601
+ ...insertTocItems,
1602
+ ...toc.replace(new RegExp(`^${EOL}`), "").split(EOL)
1603
+ );
1604
+ }
1605
+ /**
1606
+ * 更新首页目录
1607
+ * @param lines - 首页内容行数组
1608
+ * @param titles - 标题数组
1609
+ * @param titlesNotesCount - 每个标题下的笔记数量
1610
+ */
1611
+ updateHomeToc(lines, titles, titlesNotesCount) {
1612
+ let startLineIdx = -1, endLineIdx = -1;
1613
+ lines.forEach((line, idx) => {
1614
+ if (line.startsWith(NOTES_TOC_START_TAG)) startLineIdx = idx;
1615
+ if (line.startsWith(NOTES_TOC_END_TAG)) endLineIdx = idx;
1616
+ });
1617
+ if (startLineIdx === -1 || endLineIdx === -1) return;
1618
+ const toc = generateToc(titles);
1619
+ lines.splice(
1620
+ startLineIdx + 1,
1621
+ endLineIdx - startLineIdx - 1,
1622
+ ...toc.split(EOL)
1623
+ );
1624
+ }
1625
+ };
1626
+
1627
+ // core/ReadmeGenerator.ts
1628
+ var ReadmeGenerator = class {
1629
+ tocGenerator;
1630
+ configManager;
1631
+ constructor() {
1632
+ this.tocGenerator = new TocGenerator();
1633
+ this.configManager = ConfigManager.getInstance();
1634
+ }
1635
+ /**
1636
+ * 更新笔记 README
1637
+ * @param noteInfo - 笔记信息
1638
+ */
1639
+ updateNoteReadme(noteInfo) {
1640
+ if (!noteInfo.config) {
1641
+ logger.warn(`\u7B14\u8BB0 ${noteInfo.dirName} \u7F3A\u5C11\u914D\u7F6E\u6587\u4EF6`);
1642
+ return;
1643
+ }
1644
+ const content = readFileSync2(noteInfo.readmePath, "utf-8");
1645
+ if (content.length === 0) return;
1646
+ const lines = content.split(EOL);
1647
+ const repoName2 = this.configManager.get("repoName");
1648
+ this.tocGenerator.updateNoteToc(
1649
+ noteInfo.index,
1650
+ lines,
1651
+ noteInfo.config,
1652
+ repoName2
1653
+ );
1654
+ const updatedContent = lines.join(EOL);
1655
+ writeFileSync2(noteInfo.readmePath, updatedContent, "utf-8");
1656
+ }
1657
+ /**
1658
+ * 更新首页 README
1659
+ * 更新笔记链接的状态标记([x] 或 [ ]),同时更新 TOC 区域
1660
+ * @param notes - 笔记信息数组
1661
+ * @param homeReadmePath - 首页 README 路径
1662
+ */
1663
+ updateHomeReadme(notes, homeReadmePath) {
1664
+ if (!existsSync2(homeReadmePath)) {
1665
+ logger.error(`\u6839\u76EE\u5F55\u4E0B\u7684 README.md \u6587\u4EF6\u672A\u627E\u5230\uFF1A${homeReadmePath}`);
1666
+ return;
1667
+ }
1668
+ const content = readFileSync2(homeReadmePath, "utf-8");
1669
+ const lines = content.split(EOL);
1670
+ const noteByIndexMap = /* @__PURE__ */ new Map();
1671
+ for (const note of notes) {
1672
+ noteByIndexMap.set(note.index, note);
1673
+ }
1674
+ const repoOwner = this.configManager.get("author");
1675
+ const repoName2 = this.configManager.get("repoName");
1676
+ const existingNoteIndexes = /* @__PURE__ */ new Set();
1677
+ const linesToRemove = /* @__PURE__ */ new Set();
1678
+ const titles = [];
1679
+ const titlesNotesCount = [];
1680
+ let inTocRegion = false;
1681
+ let currentNoteCount = 0;
1682
+ const addNumberToTitle = createAddNumberToTitle();
1683
+ const numberedHeaders = ["## ", "### "];
1684
+ for (let i = 0; i < lines.length; i++) {
1685
+ const line = lines[i];
1686
+ if (line.includes("<!-- region:toc -->")) {
1687
+ inTocRegion = true;
1688
+ continue;
1689
+ }
1690
+ if (line.includes("<!-- endregion:toc -->")) {
1691
+ inTocRegion = false;
1692
+ continue;
1693
+ }
1694
+ if (inTocRegion) {
1695
+ continue;
1696
+ }
1697
+ const parsed = parseNoteLine(line);
1698
+ if (parsed.isMatch && parsed.noteIndex) {
1699
+ const note = noteByIndexMap.get(parsed.noteIndex);
1700
+ if (!note) {
1701
+ linesToRemove.add(i);
1702
+ logger.warn(`\u79FB\u9664\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0: ${parsed.noteIndex}`);
1703
+ continue;
1704
+ }
1705
+ existingNoteIndexes.add(parsed.noteIndex);
1706
+ lines[i] = buildNoteLineMarkdown(note, repoOwner, repoName2);
1707
+ currentNoteCount++;
1708
+ continue;
1709
+ }
1710
+ const titleMatch = line.match(/^(#{2,})\s+(.+)$/);
1711
+ if (titleMatch) {
1712
+ const isNumberedHeader = numberedHeaders.some(
1713
+ (header) => line.startsWith(header)
1714
+ );
1715
+ if (isNumberedHeader) {
1716
+ const [numberedTitle] = addNumberToTitle(line);
1717
+ lines[i] = numberedTitle;
1718
+ if (titles.length > 0) {
1719
+ titlesNotesCount.push(currentNoteCount);
1720
+ }
1721
+ titles.push(numberedTitle);
1722
+ currentNoteCount = 0;
1723
+ } else {
1724
+ if (titles.length > 0) {
1725
+ titlesNotesCount.push(currentNoteCount);
1726
+ }
1727
+ titles.push(line);
1728
+ currentNoteCount = 0;
1729
+ }
1730
+ }
1731
+ }
1732
+ const sortedLinesToRemove = Array.from(linesToRemove).sort((a, b) => b - a);
1733
+ for (const lineIndex of sortedLinesToRemove) {
1734
+ lines.splice(lineIndex, 1);
1735
+ if (currentNoteCount > 0) {
1736
+ currentNoteCount--;
1737
+ }
1738
+ }
1739
+ const missingNotes = [];
1740
+ for (const note of notes) {
1741
+ if (!existingNoteIndexes.has(note.index)) {
1742
+ missingNotes.push(note);
1743
+ }
1744
+ }
1745
+ if (missingNotes.length > 0) {
1746
+ logger.info(`\u6DFB\u52A0 ${missingNotes.length} \u7BC7\u7F3A\u5931\u7684\u7B14\u8BB0\u5230 README`);
1747
+ missingNotes.sort((a, b) => a.index.localeCompare(b.index));
1748
+ for (const note of missingNotes) {
1749
+ const noteLine = buildNoteLineMarkdown(note, repoOwner, repoName2);
1750
+ lines.push(noteLine);
1751
+ currentNoteCount++;
1752
+ }
1753
+ }
1754
+ if (titles.length > 0) {
1755
+ titlesNotesCount.push(currentNoteCount);
1756
+ }
1757
+ this.tocGenerator.updateHomeToc(lines, titles, titlesNotesCount);
1758
+ const processedLines = processEmptyLines(lines);
1759
+ const updatedContent = processedLines.join(EOL);
1760
+ writeFileSync2(homeReadmePath, updatedContent, "utf-8");
1761
+ logger.info("\u5DF2\u66F4\u65B0\u9996\u9875 README");
1762
+ }
1763
+ };
1764
+
1765
+ // core/GitManager.ts
1766
+ import { resolve as resolve2 } from "path";
1767
+ import { existsSync as existsSync3 } from "fs";
1768
+ var GitManager = class {
1769
+ logger;
1770
+ dir;
1771
+ constructor(dir, logger2) {
1772
+ this.dir = dir;
1773
+ this.logger = logger2?.child("git") || new Logger({ prefix: "git" });
1774
+ }
1775
+ /**
1776
+ * 检查是否为有效的 Git 仓库
1777
+ */
1778
+ async isValidRepo() {
1779
+ try {
1780
+ const result = await runCommand(
1781
+ "git rev-parse --is-inside-work-tree",
1782
+ this.dir
1783
+ );
1784
+ return result.trim() === "true";
1785
+ } catch {
1786
+ return false;
1787
+ }
1788
+ }
1789
+ /**
1790
+ * 确保是有效的 Git 仓库,否则抛出错误
1791
+ */
1792
+ async ensureValidRepo() {
1793
+ if (!await this.isValidRepo()) {
1794
+ throw createError.gitNotRepo(this.dir);
1795
+ }
1796
+ }
1797
+ /**
1798
+ * 获取 Git 状态
1799
+ */
1800
+ async getStatus() {
1801
+ await this.ensureValidRepo();
1802
+ const statusOutput = await runCommand(
1803
+ "git -c core.quotePath=false status --porcelain",
1804
+ this.dir
1805
+ );
1806
+ const lines = statusOutput.trim().split("\n").filter((line) => line);
1807
+ const files = lines.map((line) => {
1808
+ const statusCode = line.substring(0, 2);
1809
+ let path2 = line.substring(3);
1810
+ path2 = path2.replace(/^"(.*)"$/, "$1");
1811
+ let status = "modified";
1812
+ if (line.startsWith("??")) {
1813
+ status = "untracked";
1814
+ } else if (/^[MADRC]/.test(statusCode)) {
1815
+ status = "staged";
1816
+ } else if (/^.[MD]/.test(statusCode)) {
1817
+ status = "unstaged";
1818
+ }
1819
+ return { path: path2, status, statusCode };
1820
+ });
1821
+ const staged = files.filter((f) => f.status === "staged").length;
1822
+ const unstaged = files.filter((f) => f.status === "unstaged").length;
1823
+ const untracked = files.filter((f) => f.status === "untracked").length;
1824
+ const branch = await runCommand("git branch --show-current", this.dir);
1825
+ let ahead = 0;
1826
+ let behind = 0;
1827
+ try {
1828
+ const aheadBehind = await runCommand(
1829
+ "git rev-list --left-right --count @{upstream}...HEAD",
1830
+ this.dir
1831
+ );
1832
+ const [behindStr, aheadStr] = aheadBehind.trim().split(" ");
1833
+ behind = parseInt(behindStr) || 0;
1834
+ ahead = parseInt(aheadStr) || 0;
1835
+ } catch {
1836
+ }
1837
+ return {
1838
+ hasChanges: lines.length > 0,
1839
+ changedFiles: lines.length,
1840
+ staged,
1841
+ unstaged,
1842
+ untracked,
1843
+ branch: branch.trim(),
1844
+ ahead,
1845
+ behind,
1846
+ files
1847
+ };
1848
+ }
1849
+ /**
1850
+ * 获取远程仓库信息
1851
+ */
1852
+ async getRemoteInfo() {
1853
+ try {
1854
+ await this.ensureValidRepo();
1855
+ const remoteUrl = await runCommand(
1856
+ "git config --get remote.origin.url",
1857
+ this.dir
1858
+ );
1859
+ const url = remoteUrl.trim();
1860
+ if (!url) return null;
1861
+ const httpsMatch = url.match(
1862
+ /https:\/\/(?:www\.)?github\.com\/([^/]+)\/(.+?)(?:\.git)?$/
1863
+ );
1864
+ if (httpsMatch) {
1865
+ return {
1866
+ url,
1867
+ type: "https",
1868
+ owner: httpsMatch[1],
1869
+ repo: httpsMatch[2]
1870
+ };
1871
+ }
1872
+ const sshMatch = url.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
1873
+ if (sshMatch) {
1874
+ return {
1875
+ url,
1876
+ type: "ssh",
1877
+ owner: sshMatch[1],
1878
+ repo: sshMatch[2]
1879
+ };
1880
+ }
1881
+ return { url, type: "unknown" };
1882
+ } catch {
1883
+ return null;
1884
+ }
1885
+ }
1886
+ /**
1887
+ * 检查是否有未提交的更改
1888
+ */
1889
+ async hasUncommittedChanges() {
1890
+ const status = await this.getStatus();
1891
+ return status.hasChanges;
1892
+ }
1893
+ /**
1894
+ * Stash 当前更改
1895
+ */
1896
+ async stash(message) {
1897
+ try {
1898
+ await this.ensureValidRepo();
1899
+ const cmd = message ? `git stash push -m "${message}"` : "git stash push";
1900
+ await runCommand(cmd, this.dir);
1901
+ this.logger.info("Stashed uncommitted changes");
1902
+ return true;
1903
+ } catch (error) {
1904
+ this.logger.warn("Failed to stash changes");
1905
+ return false;
1906
+ }
1907
+ }
1908
+ /**
1909
+ * Pop stash
1910
+ */
1911
+ async stashPop() {
1912
+ try {
1913
+ await this.ensureValidRepo();
1914
+ await runCommand("git stash pop", this.dir);
1915
+ this.logger.info("Restored stashed changes");
1916
+ return true;
1917
+ } catch (error) {
1918
+ this.logger.warn("Failed to restore stashed changes");
1919
+ return false;
1920
+ }
1921
+ }
1922
+ /**
1923
+ * 拉取远程更新
1924
+ */
1925
+ async pull(options) {
1926
+ await this.ensureValidRepo();
1927
+ const { rebase = true, autostash = true } = options || {};
1928
+ const hasChanges = await this.hasUncommittedChanges();
1929
+ let didStash = false;
1930
+ if (hasChanges && !autostash) {
1931
+ this.logger.warn("Repository has uncommitted changes");
1932
+ didStash = await this.stash("Auto-stash before pull");
1933
+ }
1934
+ try {
1935
+ const status = await this.getStatus();
1936
+ const beforeCommit = await runCommand("git rev-parse HEAD", this.dir);
1937
+ this.logger.info("\u6B63\u5728\u62C9\u53D6\u8FDC\u7A0B\u66F4\u65B0...");
1938
+ const cmd = `git pull ${rebase ? "--rebase" : ""} ${autostash ? "--autostash" : ""}`.trim();
1939
+ await runCommand(cmd, this.dir);
1940
+ const afterCommit = await runCommand("git rev-parse HEAD", this.dir);
1941
+ if (beforeCommit.trim() !== afterCommit.trim()) {
1942
+ try {
1943
+ const diffOutput = await runCommand(
1944
+ `git diff --name-only ${beforeCommit.trim()}..${afterCommit.trim()}`,
1945
+ this.dir
1946
+ );
1947
+ const changedFiles = diffOutput.trim().split("\n").filter((f) => f);
1948
+ if (changedFiles.length > 0) {
1949
+ console.log(` \u66F4\u65B0\u4E86 ${changedFiles.length} \u4E2A\u6587\u4EF6:`);
1950
+ changedFiles.forEach((file, index) => {
1951
+ console.log(` ${index + 1}. ${file}`);
1952
+ });
1953
+ }
1954
+ this.logger.success(`\u62C9\u53D6\u6210\u529F: ${changedFiles.length} \u4E2A\u6587\u4EF6\u5DF2\u66F4\u65B0`);
1955
+ } catch {
1956
+ this.logger.success("\u62C9\u53D6\u6210\u529F");
1957
+ }
1958
+ } else {
1959
+ this.logger.info("\u5DF2\u662F\u6700\u65B0\uFF0C\u6CA1\u6709\u9700\u8981\u62C9\u53D6\u7684\u66F4\u65B0");
1960
+ }
1961
+ await this.updateSubmodules();
1962
+ } catch (error) {
1963
+ this.logger.error("\u62C9\u53D6\u5931\u8D25");
1964
+ handleError(error);
1965
+ throw error;
1966
+ } finally {
1967
+ if (didStash) {
1968
+ await this.stashPop();
1969
+ }
1970
+ }
1971
+ }
1972
+ /**
1973
+ * 提交更改
1974
+ */
1975
+ async commit(message) {
1976
+ await this.ensureValidRepo();
1977
+ try {
1978
+ await runCommand(`git commit -m "${message}"`, this.dir);
1979
+ this.logger.success(`Committed: ${message}`);
1980
+ } catch (error) {
1981
+ handleError(error);
1982
+ throw error;
1983
+ }
1984
+ }
1985
+ /**
1986
+ * 添加文件到暂存区
1987
+ */
1988
+ async add(files = ".") {
1989
+ await this.ensureValidRepo();
1990
+ const fileList = Array.isArray(files) ? files.join(" ") : files;
1991
+ try {
1992
+ await runCommand(`git add ${fileList}`, this.dir);
1993
+ this.logger.info(`Staged changes: ${fileList}`);
1994
+ } catch (error) {
1995
+ handleError(error);
1996
+ throw error;
1997
+ }
1998
+ }
1999
+ /**
2000
+ * 推送到远程仓库
2001
+ */
2002
+ async push(options) {
2003
+ await this.ensureValidRepo();
2004
+ const { force = false, setUpstream = false } = options || {};
2005
+ try {
2006
+ const status = await this.getStatus();
2007
+ this.logger.progress(`\u6B63\u5728\u63A8\u9001\u5230\u8FDC\u7A0B (${status.branch})...`);
2008
+ let cmd = "git push";
2009
+ if (force) cmd += " --force";
2010
+ if (setUpstream) cmd += ` --set-upstream origin ${status.branch}`;
2011
+ await runCommand(cmd, this.dir);
2012
+ const remoteInfo = await this.getRemoteInfo();
2013
+ if (remoteInfo) {
2014
+ this.logger.success(`\u63A8\u9001\u6210\u529F \u2192 ${remoteInfo.owner}/${remoteInfo.repo}`);
2015
+ } else {
2016
+ this.logger.success("\u63A8\u9001\u6210\u529F");
2017
+ }
2018
+ } catch (error) {
2019
+ this.logger.error("\u63A8\u9001\u5931\u8D25");
2020
+ handleError(error);
2021
+ throw error;
2022
+ }
2023
+ }
2024
+ /**
2025
+ * 完整的推送流程:检查 -> 添加 -> 提交 -> 推送
2026
+ */
2027
+ async pushWithCommit(commitMessage, options) {
2028
+ await this.ensureValidRepo();
2029
+ const status = await this.getStatus();
2030
+ if (!status.hasChanges) {
2031
+ this.logger.info("\u6CA1\u6709\u9700\u8981\u63D0\u4EA4\u7684\u66F4\u6539");
2032
+ return;
2033
+ }
2034
+ try {
2035
+ await this.pushSubmodules(commitMessage);
2036
+ const latestStatus = await this.getStatus();
2037
+ if (!latestStatus.hasChanges) {
2038
+ this.logger.info("\u6CA1\u6709\u9700\u8981\u63D0\u4EA4\u7684\u66F4\u6539");
2039
+ return;
2040
+ }
2041
+ this.logger.info(`\u6B63\u5728\u63A8\u9001 ${latestStatus.changedFiles} \u4E2A\u6587\u4EF6...`);
2042
+ latestStatus.files.forEach((file, index) => {
2043
+ console.log(` ${index + 1}. ${file.path}`);
2044
+ });
2045
+ await runCommand("git add .", this.dir);
2046
+ const message = commitMessage || `update: ${latestStatus.changedFiles} files modified`;
2047
+ await runCommand(`git commit -m "${message}"`, this.dir);
2048
+ let cmd = "git push";
2049
+ if (options?.force) cmd += " --force";
2050
+ await runCommand(cmd, this.dir);
2051
+ const remoteInfo = await this.getRemoteInfo();
2052
+ if (remoteInfo) {
2053
+ this.logger.success(
2054
+ `\u63A8\u9001\u6210\u529F: ${latestStatus.changedFiles} \u4E2A\u6587\u4EF6 \u2192 https://github.com/${remoteInfo.owner}/${remoteInfo.repo}`
2055
+ );
2056
+ } else {
2057
+ this.logger.success(`\u63A8\u9001\u6210\u529F: ${latestStatus.changedFiles} \u4E2A\u6587\u4EF6`);
2058
+ }
2059
+ } catch (error) {
2060
+ this.logger.error(`\u63A8\u9001\u5931\u8D25`);
2061
+ handleError(error);
2062
+ throw error;
2063
+ }
2064
+ }
2065
+ /**
2066
+ * 完整的同步流程:拉取 -> 推送
2067
+ */
2068
+ async sync(options) {
2069
+ const { commitMessage, rebase = true } = options || {};
2070
+ try {
2071
+ await this.pull({ rebase, autostash: true });
2072
+ await this.pushWithCommit(commitMessage);
2073
+ } catch (error) {
2074
+ this.logger.error("Sync failed");
2075
+ handleError(error);
2076
+ throw error;
2077
+ }
2078
+ }
2079
+ // ==================== Submodule 操作 ====================
2080
+ /**
2081
+ * 检查仓库是否包含 submodule
2082
+ */
2083
+ hasSubmodules() {
2084
+ return existsSync3(resolve2(this.dir, ".gitmodules"));
2085
+ }
2086
+ /**
2087
+ * 获取所有 submodule 的路径
2088
+ */
2089
+ async getSubmodulePaths() {
2090
+ if (!this.hasSubmodules()) return [];
2091
+ try {
2092
+ const output = await runCommand(
2093
+ "git config --file .gitmodules --get-regexp path",
2094
+ this.dir
2095
+ );
2096
+ return output.trim().split("\n").filter((line) => line).map((line) => line.replace(/^submodule\..*\.path\s+/, ""));
2097
+ } catch {
2098
+ return [];
2099
+ }
2100
+ }
2101
+ /**
2102
+ * 推送前处理 submodule:检查未提交/未推送的更改,自动提交并推送
2103
+ */
2104
+ async pushSubmodules(commitMessage) {
2105
+ const paths = await this.getSubmodulePaths();
2106
+ if (paths.length === 0) return;
2107
+ for (const subPath of paths) {
2108
+ const absPath = resolve2(this.dir, subPath);
2109
+ let hasChanges = false;
2110
+ try {
2111
+ const status = await runCommand("git status --porcelain", absPath);
2112
+ hasChanges = status.trim().length > 0;
2113
+ } catch {
2114
+ continue;
2115
+ }
2116
+ if (hasChanges) {
2117
+ const message = commitMessage || "update";
2118
+ this.logger.info(`Submodule [${subPath}] \u6709\u672A\u63D0\u4EA4\u7684\u66F4\u6539\uFF0C\u6B63\u5728\u63D0\u4EA4...`);
2119
+ await runCommand("git add -A", absPath);
2120
+ await runCommand(`git commit -m "${message}"`, absPath);
2121
+ }
2122
+ let unpushed = 0;
2123
+ try {
2124
+ const output = await runCommand(
2125
+ "git rev-list @{u}..HEAD --count",
2126
+ absPath
2127
+ );
2128
+ unpushed = parseInt(output.trim()) || 0;
2129
+ } catch {
2130
+ unpushed = 1;
2131
+ }
2132
+ if (unpushed > 0) {
2133
+ this.logger.info(
2134
+ `Submodule [${subPath}] \u6709 ${unpushed} \u4E2A\u672A\u63A8\u9001\u7684\u63D0\u4EA4\uFF0C\u6B63\u5728\u63A8\u9001...`
2135
+ );
2136
+ await runCommand("git push", absPath);
2137
+ this.logger.success(`Submodule [${subPath}] \u63A8\u9001\u6210\u529F`);
2138
+ }
2139
+ }
2140
+ }
2141
+ /**
2142
+ * 拉取后更新 submodule 到父仓库指针指向的 commit
2143
+ */
2144
+ async updateSubmodules() {
2145
+ if (!this.hasSubmodules()) return;
2146
+ try {
2147
+ this.logger.info("\u6B63\u5728\u66F4\u65B0 submodule...");
2148
+ await runCommand("git submodule update --init", this.dir);
2149
+ this.logger.success("Submodule \u5DF2\u540C\u6B65\u5230\u6700\u65B0\u6307\u9488");
2150
+ } catch (error) {
2151
+ this.logger.warn(
2152
+ "Submodule \u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u6267\u884C git submodule update --init"
2153
+ );
2154
+ }
2155
+ }
2156
+ /**
2157
+ * 显示状态摘要
2158
+ */
2159
+ async showStatus(options) {
2160
+ const { showFiles = true } = options || {};
2161
+ const status = await this.getStatus();
2162
+ const remoteInfo = await this.getRemoteInfo();
2163
+ console.log("\n\u{1F4CA} Git \u72B6\u6001:");
2164
+ console.log(` \u5206\u652F: ${status.branch}`);
2165
+ if (remoteInfo) {
2166
+ console.log(
2167
+ ` \u8FDC\u7A0B: ${remoteInfo.owner}/${remoteInfo.repo} (${remoteInfo.type})`
2168
+ );
2169
+ }
2170
+ if (status.hasChanges) {
2171
+ console.log(
2172
+ ` \u53D8\u66F4: ${status.changedFiles} \u4E2A\u6587\u4EF6 (\u5DF2\u6682\u5B58 ${status.staged}, \u672A\u6682\u5B58 ${status.unstaged}, \u672A\u8DDF\u8E2A ${status.untracked})`
2173
+ );
2174
+ if (showFiles && status.files.length > 0) {
2175
+ console.log(" \u53D8\u66F4\u6587\u4EF6\u5217\u8868:");
2176
+ const stagedFiles = status.files.filter((f) => f.status === "staged");
2177
+ const unstagedFiles = status.files.filter(
2178
+ (f) => f.status === "unstaged"
2179
+ );
2180
+ const untrackedFiles = status.files.filter(
2181
+ (f) => f.status === "untracked"
2182
+ );
2183
+ if (stagedFiles.length > 0) {
2184
+ console.log(" \u5DF2\u6682\u5B58:");
2185
+ stagedFiles.forEach((f) => console.log(` \u2713 ${f.path}`));
2186
+ }
2187
+ if (unstagedFiles.length > 0) {
2188
+ console.log(" \u672A\u6682\u5B58:");
2189
+ unstagedFiles.forEach((f) => console.log(` \u2022 ${f.path}`));
2190
+ }
2191
+ if (untrackedFiles.length > 0) {
2192
+ console.log(" \u672A\u8DDF\u8E2A:");
2193
+ untrackedFiles.forEach((f) => console.log(` ? ${f.path}`));
2194
+ }
2195
+ }
2196
+ } else {
2197
+ console.log(" \u72B6\u6001: \u5DE5\u4F5C\u533A\u5E72\u51C0\uFF0C\u6CA1\u6709\u53D8\u66F4");
2198
+ }
2199
+ if (status.ahead > 0 || status.behind > 0) {
2200
+ const syncInfo = [];
2201
+ if (status.ahead > 0) syncInfo.push(`\u9886\u5148 ${status.ahead} \u4E2A\u63D0\u4EA4`);
2202
+ if (status.behind > 0) syncInfo.push(`\u843D\u540E ${status.behind} \u4E2A\u63D0\u4EA4`);
2203
+ console.log(` \u540C\u6B65: ${syncInfo.join(", ")}`);
2204
+ }
2205
+ console.log();
2206
+ }
2207
+ };
2208
+
2209
+ // core/ProcessManager.ts
2210
+ import { spawn } from "child_process";
2211
+ var ProcessManager = class {
2212
+ processes = /* @__PURE__ */ new Map();
2213
+ logger;
2214
+ constructor() {
2215
+ this.logger = new Logger({ prefix: "process" });
2216
+ process.on("exit", () => {
2217
+ this.killAll();
2218
+ });
2219
+ process.on("SIGINT", () => {
2220
+ this.killAll();
2221
+ process.exit(0);
2222
+ });
2223
+ process.on("SIGTERM", () => {
2224
+ this.killAll();
2225
+ process.exit(0);
2226
+ });
2227
+ }
2228
+ /**
2229
+ * 启动进程
2230
+ * @param id - 进程ID
2231
+ * @param command - 命令
2232
+ * @param args - 参数列表
2233
+ * @param options - spawn 选项
2234
+ * @returns ProcessInfo
2235
+ */
2236
+ spawn(id, command, args = [], options) {
2237
+ if (this.processes.has(id)) {
2238
+ this.logger.warn(`\u8FDB\u7A0B ${id} \u5DF2\u5B58\u5728\uFF0C\u5148\u505C\u6B62\u65E7\u8FDB\u7A0B`);
2239
+ this.kill(id);
2240
+ }
2241
+ const proc = spawn(command, args, {
2242
+ stdio: "inherit",
2243
+ shell: true,
2244
+ ...options
2245
+ });
2246
+ const processInfo = {
2247
+ id,
2248
+ pid: proc.pid,
2249
+ command,
2250
+ args,
2251
+ startTime: Date.now(),
2252
+ process: proc
2253
+ };
2254
+ this.processes.set(id, processInfo);
2255
+ proc.on("exit", (code, signal) => {
2256
+ this.logger.info(`\u8FDB\u7A0B ${id} \u5DF2\u9000\u51FA (code: ${code}, signal: ${signal})`);
2257
+ this.processes.delete(id);
2258
+ });
2259
+ proc.on("error", (err) => {
2260
+ this.logger.error(`\u8FDB\u7A0B ${id} \u51FA\u9519: ${err.message}`);
2261
+ this.processes.delete(id);
2262
+ });
2263
+ return processInfo;
2264
+ }
2265
+ /**
2266
+ * 停止进程
2267
+ * @param id - 进程ID
2268
+ * @param signal - 信号(默认为 SIGTERM)
2269
+ * @returns 是否成功停止
2270
+ */
2271
+ kill(id, signal = "SIGTERM") {
2272
+ const processInfo = this.processes.get(id);
2273
+ if (!processInfo) {
2274
+ this.logger.warn(`\u8FDB\u7A0B ${id} \u4E0D\u5B58\u5728`);
2275
+ return false;
2276
+ }
2277
+ this.logger.info(`\u505C\u6B62\u8FDB\u7A0B: ${id} (PID: ${processInfo.pid})`);
2278
+ try {
2279
+ const killed = processInfo.process.kill(signal);
2280
+ if (killed) {
2281
+ this.processes.delete(id);
2282
+ return true;
2283
+ }
2284
+ return false;
2285
+ } catch (error) {
2286
+ this.logger.error(`\u505C\u6B62\u8FDB\u7A0B ${id} \u5931\u8D25: ${error}`);
2287
+ return false;
2288
+ }
2289
+ }
2290
+ /**
2291
+ * 检查进程是否存在
2292
+ * @param id - 进程ID
2293
+ * @returns 是否存在
2294
+ */
2295
+ has(id) {
2296
+ return this.processes.has(id);
2297
+ }
2298
+ /**
2299
+ * 检查进程是否在运行
2300
+ * @param id - 进程 ID
2301
+ * @returns 是否在运行
2302
+ */
2303
+ isRunning(id) {
2304
+ const processInfo = this.processes.get(id);
2305
+ if (!processInfo) return false;
2306
+ try {
2307
+ return process.kill(processInfo.pid, 0);
2308
+ } catch {
2309
+ return false;
2310
+ }
2311
+ }
2312
+ /**
2313
+ * 停止所有进程
2314
+ * @param signal - 信号(默认为 SIGTERM)
2315
+ */
2316
+ killAll(signal = "SIGTERM") {
2317
+ if (this.processes.size === 0) {
2318
+ return;
2319
+ }
2320
+ this.logger.info(`\u505C\u6B62\u6240\u6709\u8FDB\u7A0B (${this.processes.size} \u4E2A)`);
2321
+ for (const [id, processInfo] of this.processes) {
2322
+ try {
2323
+ processInfo.process.kill(signal);
2324
+ this.logger.info(`\u5DF2\u505C\u6B62\u8FDB\u7A0B: ${id}`);
2325
+ } catch (error) {
2326
+ this.logger.error(`\u505C\u6B62\u8FDB\u7A0B ${id} \u5931\u8D25: ${error}`);
2327
+ }
2328
+ }
2329
+ this.processes.clear();
2330
+ }
2331
+ };
2332
+
2333
+ // commands/BaseCommand.ts
2334
+ var BaseCommand = class {
2335
+ constructor(name) {
2336
+ this.name = name;
2337
+ this.logger = logger.child(name);
2338
+ }
2339
+ logger;
2340
+ options = {};
2341
+ /** 命令描述(从静态配置读取) */
2342
+ get description() {
2343
+ return COMMAND_DESCRIPTIONS[this.name];
2344
+ }
2345
+ /**
2346
+ * 设置命令选项
2347
+ */
2348
+ setOptions(options) {
2349
+ this.options = { ...this.options, ...options };
2350
+ }
2351
+ /**
2352
+ * 执行命令(带错误处理)
2353
+ */
2354
+ async execute() {
2355
+ const startTime = Date.now();
2356
+ try {
2357
+ this.logger.start(this.description);
2358
+ await this.run();
2359
+ const duration = Date.now() - startTime;
2360
+ this.logger.done(`\u547D\u4EE4\u6267\u884C\u8017\u65F6\uFF1A${duration} ms`);
2361
+ } catch (error) {
2362
+ handleError(error);
2363
+ throw error;
2364
+ }
2365
+ }
2366
+ };
2367
+
2368
+ // services/file-watcher/internal.ts
2369
+ var WATCH_EVENT_TYPES = {
2370
+ README: "readme",
2371
+ CONFIG: "config"
2372
+ };
2373
+ async function safeExecute(label, fn, logger2) {
2374
+ try {
2375
+ await fn();
2376
+ return true;
2377
+ } catch (error) {
2378
+ logger2.error(`[${label}] ${error}`);
2379
+ return false;
2380
+ }
2381
+ }
2382
+
2383
+ // services/file-watcher/watchState.ts
2384
+ import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync } from "fs";
2385
+ import { createHash } from "crypto";
2386
+ import { join as join4 } from "path";
2387
+ var WatchState = class {
2388
+ constructor(config2) {
2389
+ this.config = config2;
2390
+ }
2391
+ /** 文件哈希缓存 */
2392
+ fileHashes = /* @__PURE__ */ new Map();
2393
+ /** 笔记目录缓存 */
2394
+ noteDirCache = /* @__PURE__ */ new Set();
2395
+ /** 笔记配置缓存 */
2396
+ configCache = /* @__PURE__ */ new Map();
2397
+ /**
2398
+ * 获取指定文件的 MD5 哈希值,若文件不存在或读取失败返回 null
2399
+ *
2400
+ * @param filePath 文件路径
2401
+ * @returns 文件哈希
2402
+ */
2403
+ getFileHash(filePath) {
2404
+ try {
2405
+ if (!existsSync4(filePath)) return null;
2406
+ const content = readFileSync3(filePath, "utf-8");
2407
+ if (content.length === 0) return null;
2408
+ return createHash("md5").update(content).digest("hex");
2409
+ } catch {
2410
+ return null;
2411
+ }
2412
+ }
2413
+ /**
2414
+ * 更新文件哈希缓存,只有当文件内容发生变化时才更新并返回 true
2415
+ *
2416
+ * @param filePath 文件路径
2417
+ * @returns 是否发生变化
2418
+ */
2419
+ updateFileHash(filePath) {
2420
+ const current = this.getFileHash(filePath);
2421
+ if (!current) return false;
2422
+ const prev = this.fileHashes.get(filePath);
2423
+ if (prev === current) return false;
2424
+ this.fileHashes.set(filePath, current);
2425
+ return true;
2426
+ }
2427
+ /**
2428
+ * 检查指定名称的笔记目录是否已存在于缓存中
2429
+ *
2430
+ * @param name 笔记目录名称
2431
+ * @returns 若存在则返回 true,否则返回 false
2432
+ */
2433
+ hasNoteDir(name) {
2434
+ return this.noteDirCache.has(name);
2435
+ }
2436
+ /**
2437
+ * 将指定名称的笔记目录添加到缓存中
2438
+ *
2439
+ * @param name 笔记目录名称
2440
+ */
2441
+ addNoteDir(name) {
2442
+ this.noteDirCache.add(name);
2443
+ }
2444
+ /**
2445
+ * 从缓存中移除指定名称的笔记目录
2446
+ *
2447
+ * @param name 笔记目录名称
2448
+ */
2449
+ deleteNoteDir(name) {
2450
+ this.noteDirCache.delete(name);
2451
+ }
2452
+ /**
2453
+ * 清空所有缓存数据,包括文件哈希、笔记目录和配置快照
2454
+ */
2455
+ clearAll() {
2456
+ this.fileHashes.clear();
2457
+ this.noteDirCache.clear();
2458
+ this.configCache.clear();
2459
+ }
2460
+ /**
2461
+ * 清除指定笔记目录相关的缓存数据,包括 README.md 和 .tnotes.json 的文件哈希及配置快照
2462
+ *
2463
+ * @param noteDirName 笔记目录名称
2464
+ */
2465
+ clearNoteCaches(noteDirName) {
2466
+ const readmePath = join4(this.config.notesDir, noteDirName, "README.md");
2467
+ const configPath = join4(this.config.notesDir, noteDirName, ".tnotes.json");
2468
+ this.fileHashes.delete(readmePath);
2469
+ this.fileHashes.delete(configPath);
2470
+ this.configCache.delete(configPath);
2471
+ }
2472
+ /**
2473
+ * 获取指定配置文件路径对应的配置快照
2474
+ *
2475
+ * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
2476
+ * @returns 配置快照,若不存在则返回 undefined
2477
+ */
2478
+ getConfigSnapshot(configPath) {
2479
+ return this.configCache.get(configPath);
2480
+ }
2481
+ /**
2482
+ * 设置指定配置文件路径的配置快照到缓存中
2483
+ *
2484
+ * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
2485
+ * @param snapshot 配置快照对象
2486
+ */
2487
+ setConfigSnapshot(configPath, snapshot) {
2488
+ this.configCache.set(configPath, snapshot);
2489
+ }
2490
+ /**
2491
+ * 读取指定配置文件的快照
2492
+ *
2493
+ * 解析 .tnotes.json 配置文件,提取 done、enableDiscussions、description 字段。
2494
+ *
2495
+ * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
2496
+ * @returns 配置快照,若文件不存在或解析失败则返回 null
2497
+ */
2498
+ readConfigSnapshot(configPath) {
2499
+ try {
2500
+ if (!existsSync4(configPath)) return null;
2501
+ const content = readFileSync3(configPath, "utf-8");
2502
+ const config2 = JSON.parse(content);
2503
+ return {
2504
+ done: Boolean(config2.done),
2505
+ enableDiscussions: Boolean(config2.enableDiscussions),
2506
+ description: config2.description || ""
2507
+ };
2508
+ } catch (error) {
2509
+ this.config.logger.error(`[\u8BFB\u53D6\u914D\u7F6E\u5FEB\u7167] ${error}`);
2510
+ return null;
2511
+ }
2512
+ }
2513
+ /**
2514
+ * 从磁盘初始化监听状态缓存:
2515
+ * 遍历笔记根目录下的所有子目录,将每个笔记目录的 README.md 和 .tnotes.json
2516
+ * 的哈希值及配置快照加载到缓存中。
2517
+ */
2518
+ initializeFromDisk() {
2519
+ try {
2520
+ const noteDirs = readdirSync3(this.config.notesDir);
2521
+ this.clearAll();
2522
+ for (const noteDir of noteDirs) {
2523
+ const noteDirPath = join4(this.config.notesDir, noteDir);
2524
+ if (!statSync(noteDirPath).isDirectory()) continue;
2525
+ this.noteDirCache.add(noteDir);
2526
+ const readmePath = join4(noteDirPath, "README.md");
2527
+ const readmeHash = this.getFileHash(readmePath);
2528
+ if (readmeHash) this.fileHashes.set(readmePath, readmeHash);
2529
+ const configPath = join4(noteDirPath, ".tnotes.json");
2530
+ const configHash = this.getFileHash(configPath);
2531
+ if (configHash) {
2532
+ this.fileHashes.set(configPath, configHash);
2533
+ const snapshot = this.readConfigSnapshot(configPath);
2534
+ if (snapshot) this.configCache.set(configPath, snapshot);
2535
+ }
2536
+ }
2537
+ } catch (error) {
2538
+ this.config.logger.warn(
2539
+ `[initializeFromDisk] \u521D\u59CB\u5316\u76D1\u542C\u72B6\u6001\u5931\u8D25: ${error}`
2540
+ );
2541
+ }
2542
+ }
2543
+ };
2544
+
2545
+ // services/file-watcher/eventScheduler.ts
2546
+ var DEFAULT_DEBOUNCE_MS = 1e3;
2547
+ var DEFAULT_BATCH_WINDOW_MS = 1e3;
2548
+ var DEFAULT_BATCH_THRESHOLD = 3;
2549
+ var DEFAULT_BATCH_BUFFER_MS = 2e3;
2550
+ var EventScheduler = class {
2551
+ constructor(config2) {
2552
+ this.config = config2;
2553
+ this.debounceMs = config2.debounceMs ?? DEFAULT_DEBOUNCE_MS;
2554
+ this.batchWindowMs = config2.batchWindowMs ?? DEFAULT_BATCH_WINDOW_MS;
2555
+ this.batchThreshold = config2.batchThreshold ?? DEFAULT_BATCH_THRESHOLD;
2556
+ this.batchBufferMs = config2.batchBufferMs ?? DEFAULT_BATCH_BUFFER_MS;
2557
+ }
2558
+ /** 待处理的文件变更事件队列 */
2559
+ pendingEvents = /* @__PURE__ */ new Map();
2560
+ /** 防抖定时器 */
2561
+ updateTimer = null;
2562
+ /** 批量更新恢复定时器 */
2563
+ batchResumeTimer = null;
2564
+ /** 记录最近的变更时间戳 */
2565
+ recentChanges = [];
2566
+ /** 标记是否正在更新,避免循环触发 - 类似一把更新行为锁 */
2567
+ isUpdating = false;
2568
+ debounceMs;
2569
+ batchWindowMs;
2570
+ batchThreshold;
2571
+ batchBufferMs;
2572
+ /**
2573
+ * 设置更新状态锁,用于防止在执行耗时更新操作时被新的文件变更事件打断
2574
+ *
2575
+ * @param flag - true 表示正在更新(锁定),false 表示更新完成(解锁)
2576
+ */
2577
+ setUpdating(flag) {
2578
+ this.isUpdating = flag;
2579
+ }
2580
+ /**
2581
+ * 获取当前是否处于更新锁定状态
2582
+ *
2583
+ * @returns true 表示正在执行更新操作(事件处理被暂停),false 表示空闲可处理新事件
2584
+ */
2585
+ getUpdating() {
2586
+ return this.isUpdating;
2587
+ }
2588
+ /**
2589
+ * 将文件变更事件加入待处理队列,并启动防抖定时器
2590
+ *
2591
+ * - 若同一文件路径的事件已存在,则忽略重复事件(去重)
2592
+ * - 每次新事件都会重置防抖计时器,确保在变更停止后才触发处理
2593
+ *
2594
+ * @param event 文件变更事件
2595
+ */
2596
+ enqueue(event) {
2597
+ if (this.pendingEvents.has(event.path)) return;
2598
+ this.pendingEvents.set(event.path, event);
2599
+ if (this.updateTimer) clearTimeout(this.updateTimer);
2600
+ this.updateTimer = setTimeout(() => this.flush(), this.debounceMs);
2601
+ }
2602
+ /**
2603
+ * 立即触发事件队列的处理(防抖到期或手动调用)
2604
+ *
2605
+ * - 若当前正在更新(isUpdating 为 true),则跳过以避免重复处理
2606
+ * - 清空待处理事件队列,并通过 onFlush 回调交由上层服务处理
2607
+ * - 处理开始后会锁定更新状态,防止处理过程中被新事件打断
2608
+ */
2609
+ flush() {
2610
+ if (this.isUpdating) return;
2611
+ if (this.pendingEvents.size === 0) return;
2612
+ const events = Array.from(this.pendingEvents.values());
2613
+ this.pendingEvents.clear();
2614
+ this.isUpdating = true;
2615
+ this.config.onFlush(events);
2616
+ }
2617
+ /**
2618
+ * 记录当前变更时间并检测是否触发批量更新模式
2619
+ *
2620
+ * - 维护一个滑动时间窗口(BATCH_UPDATE_WINDOW_MS)内的变更记录
2621
+ * - 若短时间内(1秒内)变更次数达到阈值(BATCH_UPDATE_THRESHOLD = 3),则判定为批量操作
2622
+ * - 触发批量模式后:
2623
+ * 1. 清空当前待处理事件队列,避免重复处理
2624
+ * 2. 锁定更新状态(isUpdating = true)
2625
+ * 3. 暂停监听服务,并在延迟(窗口 + 缓冲时间)后自动恢复
2626
+ *
2627
+ * @param now 当前时间戳(默认使用 Date.now())
2628
+ * @returns true 表示已触发批量更新模式,false 表示仍处于普通监听模式
2629
+ */
2630
+ recordChangeAndDetectBatch(now = Date.now()) {
2631
+ this.recentChanges.push(now);
2632
+ this.recentChanges = this.recentChanges.filter(
2633
+ (t) => now - t < this.batchWindowMs
2634
+ );
2635
+ if (this.recentChanges.length < this.batchThreshold) return false;
2636
+ this.pendingEvents.clear();
2637
+ this.recentChanges = [];
2638
+ this.isUpdating = true;
2639
+ this.config.onPauseForBatch();
2640
+ this.batchResumeTimer = setTimeout(() => {
2641
+ this.batchResumeTimer = null;
2642
+ this.isUpdating = false;
2643
+ this.config.reinit();
2644
+ this.config.onResumeAfterBatch();
2645
+ }, this.batchWindowMs + this.batchBufferMs);
2646
+ return true;
2647
+ }
2648
+ /**
2649
+ * 清理所有定时器,释放资源
2650
+ *
2651
+ * 在服务停止时调用,防止定时器在服务销毁后仍然触发回调
2652
+ */
2653
+ clearTimers() {
2654
+ if (this.updateTimer) {
2655
+ clearTimeout(this.updateTimer);
2656
+ this.updateTimer = null;
2657
+ }
2658
+ if (this.batchResumeTimer) {
2659
+ clearTimeout(this.batchResumeTimer);
2660
+ this.batchResumeTimer = null;
2661
+ }
2662
+ this.pendingEvents.clear();
2663
+ this.recentChanges = [];
2664
+ }
2665
+ };
2666
+
2667
+ // services/file-watcher/renameDetector.ts
2668
+ import { existsSync as existsSync5 } from "fs";
2669
+ import { join as join5 } from "path";
2670
+ var FOLDER_RENAME_DETECT_WINDOW_MS = 500;
2671
+ var RenameDetector = class {
2672
+ constructor(config2) {
2673
+ this.config = config2;
2674
+ }
2675
+ /** 待处理的文件夹重命名 */
2676
+ pendingFolderRename = null;
2677
+ /** 文件夹重命名检测定时器 */
2678
+ folderRenameTimer = null;
2679
+ handleFsRename(folderName) {
2680
+ const { notesDir, dirCache, logger: logger2, onDelete, onRename } = this.config;
2681
+ const folderPath = join5(notesDir, folderName);
2682
+ const folderExists = existsSync5(folderPath);
2683
+ const noteIndex = NoteManager.extractNoteIndex(folderName);
2684
+ if (!noteIndex) {
2685
+ logger2.warn(`\u65E0\u6CD5\u4ECE\u6587\u4EF6\u5939\u540D\u79F0\u63D0\u53D6\u7B14\u8BB0\u7D22\u5F15: ${folderName}`);
2686
+ return;
2687
+ }
2688
+ if (!folderExists) {
2689
+ if (dirCache.has(folderName)) {
2690
+ logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u5220\u9664/\u91CD\u547D\u540D: ${folderName}`);
2691
+ this.pendingFolderRename = { oldName: folderName, time: Date.now() };
2692
+ if (this.folderRenameTimer) clearTimeout(this.folderRenameTimer);
2693
+ this.folderRenameTimer = setTimeout(() => {
2694
+ if (this.pendingFolderRename) {
2695
+ logger2.warn(`\u68C0\u6D4B\u5230\u7B14\u8BB0\u5220\u9664: ${this.pendingFolderRename.oldName}`);
2696
+ onDelete(this.pendingFolderRename.oldName);
2697
+ }
2698
+ this.pendingFolderRename = null;
2699
+ this.folderRenameTimer = null;
2700
+ }, FOLDER_RENAME_DETECT_WINDOW_MS);
2701
+ }
2702
+ return;
2703
+ }
2704
+ if (!dirCache.has(folderName)) {
2705
+ logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u521B\u5EFA/\u91CD\u547D\u540D: ${folderName}`);
2706
+ if (this.pendingFolderRename && Date.now() - this.pendingFolderRename.time < FOLDER_RENAME_DETECT_WINDOW_MS) {
2707
+ const oldName = this.pendingFolderRename.oldName;
2708
+ const oldNoteIndex = NoteManager.extractNoteIndex(oldName);
2709
+ if (oldNoteIndex && oldNoteIndex === noteIndex) {
2710
+ logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u91CD\u547D\u540D: ${oldName} \u2192 ${folderName}`);
2711
+ if (this.folderRenameTimer) {
2712
+ clearTimeout(this.folderRenameTimer);
2713
+ this.folderRenameTimer = null;
2714
+ }
2715
+ onRename(oldName, folderName);
2716
+ this.pendingFolderRename = null;
2717
+ } else if (oldNoteIndex && oldNoteIndex !== noteIndex) {
2718
+ logger2.warn(`\u7D22\u5F15\u51B2\u7A81\uFF0C\u56DE\u9000: ${oldName} -> ${folderName}`);
2719
+ }
2720
+ }
2721
+ dirCache.add(folderName);
2722
+ if (this.pendingFolderRename) {
2723
+ dirCache.delete(this.pendingFolderRename.oldName);
2724
+ }
2725
+ }
2726
+ }
2727
+ clearTimers() {
2728
+ if (this.folderRenameTimer) {
2729
+ clearTimeout(this.folderRenameTimer);
2730
+ this.folderRenameTimer = null;
2731
+ }
2732
+ this.pendingFolderRename = null;
2733
+ }
2734
+ };
2735
+
2736
+ // services/file-watcher/configChangeHandler.ts
2737
+ var ConfigChangeHandler = class {
2738
+ constructor(config2) {
2739
+ this.config = config2;
2740
+ }
2741
+ async handle(events) {
2742
+ if (events.length === 0) return [];
2743
+ const changedIndexes = [];
2744
+ const { state, noteService, noteIndexCache, logger: logger2 } = this.config;
2745
+ for (const change of events) {
2746
+ if (noteService.shouldIgnoreConfigChange(change.path)) {
2747
+ logger2.debug(`\u5FFD\u7565 API \u5199\u5165\u7684\u914D\u7F6E\u6587\u4EF6: ${change.path}`);
2748
+ continue;
2749
+ }
2750
+ const snapshot = state.readConfigSnapshot(change.path);
2751
+ if (!snapshot) continue;
2752
+ const cached = state.getConfigSnapshot(change.path);
2753
+ state.setConfigSnapshot(change.path, snapshot);
2754
+ noteIndexCache.updateConfig(change.noteIndex, snapshot);
2755
+ if (!cached) continue;
2756
+ const statusChanged = cached.done !== snapshot.done;
2757
+ const otherChanged = cached.enableDiscussions !== snapshot.enableDiscussions || cached.description !== snapshot.description;
2758
+ if (statusChanged) {
2759
+ changedIndexes.push(change.noteIndex);
2760
+ logger2.info(`\u68C0\u6D4B\u5230\u914D\u7F6E\u72B6\u6001\u53D8\u5316: done(${cached.done}\u2192${snapshot.done})`);
2761
+ } else if (otherChanged) {
2762
+ logger2.info("\u68C0\u6D4B\u5230\u914D\u7F6E\u975E\u72B6\u6001\u5B57\u6BB5\u53D8\u5316\uFF0C\u5DF2\u5237\u65B0\u7F13\u5B58\uFF08\u65E0\u9700\u5168\u5C40\u66F4\u65B0\uFF09");
2763
+ }
2764
+ }
2765
+ return changedIndexes;
2766
+ }
2767
+ };
2768
+
2769
+ // services/file-watcher/readmeChangeHandler.ts
2770
+ var ReadmeChangeHandler = class {
2771
+ constructor(config2) {
2772
+ this.config = config2;
2773
+ }
2774
+ async handle(events) {
2775
+ if (events.length === 0) return;
2776
+ const indexes = [...new Set(events.map((c) => c.noteIndex))];
2777
+ for (const noteIndex of indexes) {
2778
+ const noteInfo = this.config.noteService.getNoteByIndex(noteIndex);
2779
+ if (noteInfo) {
2780
+ await this.config.noteService.fixNoteTitle(noteInfo);
2781
+ }
2782
+ }
2783
+ }
2784
+ };
2785
+
2786
+ // services/file-watcher/globalUpdateCoordinator.ts
2787
+ var GlobalUpdateCoordinator = class {
2788
+ constructor(config2) {
2789
+ this.config = config2;
2790
+ }
2791
+ async applyConfigUpdates(changedNoteIndexes) {
2792
+ if (changedNoteIndexes.length === 0) return;
2793
+ const { readmeService, noteIndexCache, logger: logger2 } = this.config;
2794
+ logger2.info("\u68C0\u6D4B\u5230\u7B14\u8BB0\u72B6\u6001\u53D8\u5316\uFF0C\u589E\u91CF\u66F4\u65B0\u5168\u5C40\u6587\u4EF6...");
2795
+ for (const noteIndex of changedNoteIndexes) {
2796
+ await safeExecute(
2797
+ `\u589E\u91CF\u66F4\u65B0 ${noteIndex}`,
2798
+ async () => {
2799
+ const item = noteIndexCache.getByNoteIndex(noteIndex);
2800
+ await readmeService.updateNoteInReadme(
2801
+ noteIndex,
2802
+ item?.noteConfig || {}
2803
+ );
2804
+ logger2.info(`\u589E\u91CF\u66F4\u65B0 README \u4E2D\u7684\u7B14\u8BB0: ${noteIndex}`);
2805
+ },
2806
+ logger2
2807
+ );
2808
+ }
2809
+ await readmeService.regenerateSidebar();
2810
+ }
2811
+ async updateNoteReadmesOnly(events) {
2812
+ const noteIndexesToUpdate = [...new Set(events.map((c) => c.noteIndex))];
2813
+ if (noteIndexesToUpdate.length === 0) return;
2814
+ await this.config.readmeService.updateNoteReadmesOnly(noteIndexesToUpdate);
2815
+ }
2816
+ };
2817
+
2818
+ // services/file-watcher/folderChangeHandler.ts
2819
+ import { existsSync as existsSync6, promises as fsPromises } from "fs";
2820
+ import { join as join6 } from "path";
2821
+ var RENAME_REVERT_DELAY_MS = 2e3;
2822
+ var DELETE_REINIT_DELAY_MS = 1e3;
2823
+ var UPDATE_UNLOCK_DELAY_MS = 500;
2824
+ var FolderChangeHandler = class {
2825
+ constructor(config2) {
2826
+ this.config = config2;
2827
+ }
2828
+ /** 活跃的定时器 ID 集合,用于统一清理 */
2829
+ activeTimers = /* @__PURE__ */ new Set();
2830
+ /**
2831
+ * 清理所有活跃定时器,释放资源
2832
+ *
2833
+ * 在服务停止时调用,防止定时器在服务销毁后仍然触发回调
2834
+ */
2835
+ clearTimers() {
2836
+ for (const timer of this.activeTimers) {
2837
+ clearTimeout(timer);
2838
+ }
2839
+ this.activeTimers.clear();
2840
+ }
2841
+ scheduleTimer(fn, delay) {
2842
+ const timer = setTimeout(() => {
2843
+ this.activeTimers.delete(timer);
2844
+ fn();
2845
+ }, delay);
2846
+ this.activeTimers.add(timer);
2847
+ }
2848
+ async handleFolderDeletion(deletedFolderName) {
2849
+ const { scheduler, watchState, noteIndexCache, readmeService, logger: logger2 } = this.config;
2850
+ if (scheduler.getUpdating()) return;
2851
+ scheduler.setUpdating(true);
2852
+ try {
2853
+ const noteIndex = this.extractNoteIndexOrWarn(deletedFolderName);
2854
+ if (!noteIndex) return;
2855
+ logger2.info(`\u6B63\u5728\u5904\u7406\u7B14\u8BB0\u5220\u9664: ${noteIndex} (${deletedFolderName})`);
2856
+ watchState.deleteNoteDir(deletedFolderName);
2857
+ watchState.clearNoteCaches(deletedFolderName);
2858
+ noteIndexCache.delete(noteIndex);
2859
+ await safeExecute(
2860
+ `\u5220\u9664\u7B14\u8BB0 ${noteIndex}`,
2861
+ async () => {
2862
+ await readmeService.deleteNoteFromReadme(noteIndex);
2863
+ await readmeService.regenerateSidebar();
2864
+ },
2865
+ logger2
2866
+ );
2867
+ } finally {
2868
+ this.scheduleTimer(() => {
2869
+ scheduler.setUpdating(false);
2870
+ watchState.initializeFromDisk();
2871
+ }, DELETE_REINIT_DELAY_MS);
2872
+ }
2873
+ }
2874
+ async handleFolderRenameUpdate(oldName, newName) {
2875
+ const { scheduler, watchState, logger: logger2 } = this.config;
2876
+ if (scheduler.getUpdating()) return;
2877
+ scheduler.setUpdating(true);
2878
+ try {
2879
+ const { oldNoteIndex, newNoteIndex } = this.validateRenameIndexes(
2880
+ oldName,
2881
+ newName
2882
+ );
2883
+ if (!oldNoteIndex || !newNoteIndex) return;
2884
+ logger2.info(`\u6B63\u5728\u5904\u7406\u6587\u4EF6\u5939\u91CD\u547D\u540D: ${oldName} \u2192 ${newName}`);
2885
+ if (oldNoteIndex === newNoteIndex) {
2886
+ await safeExecute(
2887
+ `\u6807\u9898\u91CD\u547D\u540D ${newNoteIndex}`,
2888
+ () => this.handleTitleOnlyRename(newNoteIndex, newName),
2889
+ logger2
2890
+ );
2891
+ } else {
2892
+ await safeExecute(
2893
+ `\u7D22\u5F15\u53D8\u66F4 ${oldNoteIndex}\u2192${newNoteIndex}`,
2894
+ () => this.handleIndexChangedRename(oldNoteIndex, newNoteIndex),
2895
+ logger2
2896
+ );
2897
+ }
2898
+ } finally {
2899
+ this.scheduleTimer(() => {
2900
+ scheduler.setUpdating(false);
2901
+ watchState.initializeFromDisk();
2902
+ }, UPDATE_UNLOCK_DELAY_MS);
2903
+ }
2904
+ }
2905
+ // #region - 私有实现
2906
+ validateRenameIndexes(oldName, newName) {
2907
+ const { noteIndexCache, logger: logger2 } = this.config;
2908
+ const oldNoteIndex = this.extractNoteIndexOrWarn(oldName);
2909
+ const newNoteIndex = this.extractNoteIndexOrWarn(newName);
2910
+ if (!oldNoteIndex || !newNoteIndex) {
2911
+ return { oldNoteIndex: null, newNoteIndex: null };
2912
+ }
2913
+ if (!/^\d{4}$/.test(newNoteIndex)) {
2914
+ logger2.error(`\u65B0\u7B14\u8BB0\u7D22\u5F15\u683C\u5F0F\u975E\u6CD5: ${newNoteIndex}\uFF0C\u81EA\u52A8\u56DE\u9000`);
2915
+ this.revertFolderRename(oldName, newName);
2916
+ return { oldNoteIndex: null, newNoteIndex: null };
2917
+ }
2918
+ if (oldNoteIndex !== newNoteIndex && noteIndexCache.has(newNoteIndex)) {
2919
+ logger2.error(`\u65B0\u7B14\u8BB0\u7D22\u5F15 ${newNoteIndex} \u5DF2\u5B58\u5728\uFF0C\u81EA\u52A8\u56DE\u9000`);
2920
+ this.revertFolderRename(oldName, newName);
2921
+ return { oldNoteIndex: null, newNoteIndex: null };
2922
+ }
2923
+ return { oldNoteIndex, newNoteIndex };
2924
+ }
2925
+ async handleTitleOnlyRename(noteIndex, newName) {
2926
+ const { noteIndexCache, readmeService, logger: logger2 } = this.config;
2927
+ logger2.info(`\u7B14\u8BB0\u7D22\u5F15\u672A\u53D8 (${noteIndex})\uFF0C\u53EA\u66F4\u65B0\u6807\u9898`);
2928
+ noteIndexCache.updateFolderName(noteIndex, newName);
2929
+ const item = noteIndexCache.getByNoteIndex(noteIndex);
2930
+ if (item) {
2931
+ await readmeService.updateNoteInReadme(noteIndex, item.noteConfig);
2932
+ }
2933
+ await readmeService.regenerateSidebar();
2934
+ logger2.success(`\u6807\u9898\u66F4\u65B0\u5B8C\u6210`);
2935
+ }
2936
+ async handleIndexChangedRename(oldNoteIndex, newNoteIndex) {
2937
+ const { noteService, noteIndexCache, readmeService, logger: logger2 } = this.config;
2938
+ logger2.info(`\u7B14\u8BB0\u7D22\u5F15\u53D8\u66F4: ${oldNoteIndex} \u2192 ${newNoteIndex}`);
2939
+ await readmeService.deleteNoteFromReadme(oldNoteIndex);
2940
+ const newNote = noteService.getNoteByIndex(newNoteIndex);
2941
+ if (newNote) {
2942
+ noteIndexCache.delete(oldNoteIndex);
2943
+ noteIndexCache.add(newNote);
2944
+ await readmeService.appendNoteToReadme(newNoteIndex);
2945
+ await readmeService.regenerateSidebar();
2946
+ logger2.success(`\u7B14\u8BB0\u7D22\u5F15\u53D8\u66F4\u5904\u7406\u5B8C\u6210`);
2947
+ } else {
2948
+ logger2.error(`\u672A\u627E\u5230\u65B0\u7B14\u8BB0: ${newNoteIndex}`);
2949
+ }
2950
+ }
2951
+ async revertFolderRename(oldName, newName) {
2952
+ const { notesDir, scheduler, watchState, logger: logger2 } = this.config;
2953
+ try {
2954
+ const oldPath = join6(notesDir, oldName);
2955
+ const newPath = join6(notesDir, newName);
2956
+ if (existsSync6(newPath)) {
2957
+ scheduler.setUpdating(true);
2958
+ await fsPromises.rename(newPath, oldPath);
2959
+ logger2.warn(`\u6587\u4EF6\u5939\u5DF2\u56DE\u9000: ${newName} \u2192 ${oldName}`);
2960
+ this.scheduleTimer(() => {
2961
+ scheduler.setUpdating(false);
2962
+ watchState.initializeFromDisk();
2963
+ }, RENAME_REVERT_DELAY_MS);
2964
+ }
2965
+ } catch (error) {
2966
+ logger2.error(`\u56DE\u9000\u6587\u4EF6\u5939\u91CD\u547D\u540D\u5931\u8D25: ${error}`);
2967
+ }
2968
+ }
2969
+ extractNoteIndexOrWarn(name) {
2970
+ const noteIndex = name.match(/^(\d{4})/)?.[1] || null;
2971
+ if (!noteIndex) {
2972
+ this.config.logger.warn(`\u65E0\u6CD5\u4ECE\u6587\u4EF6\u5939\u540D\u79F0\u63D0\u53D6\u7B14\u8BB0\u7D22\u5F15: ${name}`);
2973
+ }
2974
+ return noteIndex;
2975
+ }
2976
+ // #endregion - 私有实现
2977
+ };
2978
+
2979
+ // services/file-watcher/fsWatcherAdapter.ts
2980
+ import { watch } from "fs";
2981
+ import { basename, dirname, join as join7, sep } from "path";
2982
+ var FsWatcherAdapter = class {
2983
+ constructor(config2) {
2984
+ this.config = config2;
2985
+ }
2986
+ /** 文件系统监听器实例 */
2987
+ watcher = null;
2988
+ start() {
2989
+ const { logger: logger2 } = this.config;
2990
+ if (this.watcher) {
2991
+ logger2.warn("\u6587\u4EF6\u76D1\u542C\u670D\u52A1\u5DF2\u542F\u52A8");
2992
+ return;
2993
+ }
2994
+ this.watcher = watch(
2995
+ this.config.notesDir,
2996
+ { recursive: true },
2997
+ (eventType, filename) => this.handleFsEvent(eventType, filename)
2998
+ );
2999
+ logger2.debug(`\u6587\u4EF6\u76D1\u542C\u5DF2\u542F\u52A8\uFF0C\u76D1\u542C\u76EE\u5F55\uFF1A${this.config.notesDir}`);
3000
+ }
3001
+ stop() {
3002
+ if (!this.watcher) return;
3003
+ this.watcher.close();
3004
+ this.watcher = null;
3005
+ }
3006
+ isWatching() {
3007
+ return this.watcher !== null;
3008
+ }
3009
+ handleFsEvent(eventType, filename) {
3010
+ const { isUpdating, onRename, onNoteEvent } = this.config;
3011
+ if (!filename || // 忽略无文件变更
3012
+ isUpdating()) {
3013
+ return;
3014
+ }
3015
+ if (eventType === "rename" && // 检测文件夹 rename 事件
3016
+ !filename.includes(sep)) {
3017
+ onRename(filename);
3018
+ return;
3019
+ }
3020
+ const baseFilename = basename(filename);
3021
+ if (baseFilename !== "README.md" && baseFilename !== ".tnotes.json") {
3022
+ return;
3023
+ }
3024
+ const fullPath = join7(this.config.notesDir, filename);
3025
+ const event = this.buildWatchEvent(fullPath, filename);
3026
+ if (!event) {
3027
+ return;
3028
+ }
3029
+ onNoteEvent(event);
3030
+ }
3031
+ buildWatchEvent(fullPath, filename) {
3032
+ const noteDirName = basename(dirname(fullPath));
3033
+ const noteIndex = NoteManager.extractNoteIndex(noteDirName);
3034
+ if (!noteIndex) {
3035
+ NoteManager.warnInvalidNoteIndex(noteDirName);
3036
+ return null;
3037
+ }
3038
+ const fileType = filename.endsWith("README.md") ? WATCH_EVENT_TYPES.README : WATCH_EVENT_TYPES.CONFIG;
3039
+ return {
3040
+ path: fullPath,
3041
+ type: fileType,
3042
+ noteIndex,
3043
+ noteDirName,
3044
+ noteDirPath: dirname(fullPath)
3045
+ };
3046
+ }
3047
+ };
3048
+
3049
+ // services/readme/service.ts
3050
+ import {
3051
+ existsSync as existsSync7,
3052
+ readFileSync as readFileSync4,
3053
+ writeFileSync as writeFileSync3,
3054
+ promises as fsPromises2
3055
+ } from "fs";
3056
+ var ReadmeService = class _ReadmeService {
3057
+ static instance;
3058
+ noteManager;
3059
+ readmeGenerator;
3060
+ configManager;
3061
+ noteIndexCache;
3062
+ constructor() {
3063
+ this.noteManager = NoteManager.getInstance();
3064
+ this.readmeGenerator = new ReadmeGenerator();
3065
+ this.configManager = ConfigManager.getInstance();
3066
+ this.noteIndexCache = NoteIndexCache.getInstance();
3067
+ }
3068
+ static getInstance() {
3069
+ if (!_ReadmeService.instance) {
3070
+ _ReadmeService.instance = new _ReadmeService();
3071
+ }
3072
+ return _ReadmeService.instance;
3073
+ }
3074
+ /**
3075
+ * 更新所有笔记的 README
3076
+ * @param options - 更新选项
3077
+ */
3078
+ async updateAllReadmes(options = {}) {
3079
+ const {
3080
+ updateSidebar = true,
3081
+ updateHome = true,
3082
+ notes: providedNotes
3083
+ } = options;
3084
+ logger.info("\u5F00\u59CB\u66F4\u65B0\u77E5\u8BC6\u5E93...");
3085
+ const notes = providedNotes ?? this.noteManager.scanNotes();
3086
+ logger.info(`\u626B\u63CF\u5230 ${notes.length} \u7BC7\u7B14\u8BB0`);
3087
+ const changedIndexes = await this.getChangedNoteIndexes();
3088
+ const shouldIncrementalUpdate = changedIndexes.size > 0 && changedIndexes.size < notes.length * 0.3;
3089
+ let notesToUpdate = notes;
3090
+ if (shouldIncrementalUpdate) {
3091
+ notesToUpdate = notes.filter((note) => changedIndexes.has(note.index));
3092
+ logger.info(
3093
+ `\u68C0\u6D4B\u5230 ${changedIndexes.size} \u7BC7\u7B14\u8BB0\u6709\u53D8\u66F4\uFF0C\u4F7F\u7528\u589E\u91CF\u66F4\u65B0\u6A21\u5F0F`
3094
+ );
3095
+ } else {
3096
+ logger.info("\u4F7F\u7528\u5168\u91CF\u66F4\u65B0\u6A21\u5F0F");
3097
+ }
3098
+ const startTime = Date.now();
3099
+ await this.updateNoteReadmesInParallel(notesToUpdate);
3100
+ const updateTime = Date.now() - startTime;
3101
+ logger.info(`\u66F4\u65B0\u4E86 ${notesToUpdate.length} \u7BC7\u7B14\u8BB0 (\u8017\u65F6 ${updateTime}ms)`);
3102
+ if (updateHome) {
3103
+ await this.updateHomeReadme(notes);
3104
+ }
3105
+ if (updateSidebar) {
3106
+ await this.updateSidebar(notes);
3107
+ }
3108
+ logger.info("\u77E5\u8BC6\u5E93\u66F4\u65B0\u5B8C\u6210\uFF01");
3109
+ }
3110
+ /**
3111
+ * 只更新指定笔记的 README(不更新 sidebar、home)
3112
+ * @param noteIndexes - 笔记索引数组,例如 ['0001', '0002']
3113
+ */
3114
+ async updateNoteReadmesOnly(noteIndexes) {
3115
+ if (noteIndexes.length === 0) return;
3116
+ const notesToUpdate = [];
3117
+ for (const noteIndex of noteIndexes) {
3118
+ const note = this.noteManager.getNoteByIndex(noteIndex);
3119
+ if (note) {
3120
+ notesToUpdate.push(note);
3121
+ } else {
3122
+ logger.warn(`\u7B14\u8BB0\u672A\u627E\u5230: ${noteIndex}`);
3123
+ }
3124
+ }
3125
+ if (notesToUpdate.length === 0) {
3126
+ logger.warn("\u6CA1\u6709\u627E\u5230\u9700\u8981\u66F4\u65B0\u7684\u7B14\u8BB0");
3127
+ return;
3128
+ }
3129
+ for (const note of notesToUpdate) {
3130
+ try {
3131
+ this.readmeGenerator.updateNoteReadme(note);
3132
+ } catch (error) {
3133
+ logger.error(`\u66F4\u65B0\u7B14\u8BB0 ${note.dirName} \u5931\u8D25`, error);
3134
+ }
3135
+ }
3136
+ }
3137
+ /**
3138
+ * 获取变更的笔记索引集合
3139
+ * @returns 变更的笔记索引集合
3140
+ */
3141
+ async getChangedNoteIndexes() {
3142
+ try {
3143
+ return getChangedIds();
3144
+ } catch (error) {
3145
+ return /* @__PURE__ */ new Set();
3146
+ }
3147
+ }
3148
+ /**
3149
+ * 并行更新多个笔记的 README
3150
+ * @param notes - 笔记信息数组
3151
+ */
3152
+ async updateNoteReadmesInParallel(notes) {
3153
+ const batchSize = 10;
3154
+ const batches = [];
3155
+ for (let i = 0; i < notes.length; i += batchSize) {
3156
+ batches.push(notes.slice(i, i + batchSize));
3157
+ }
3158
+ let successCount = 0;
3159
+ let failCount = 0;
3160
+ for (const batch of batches) {
3161
+ const results = await Promise.allSettled(
3162
+ batch.map(
3163
+ (note) => Promise.resolve().then(() => {
3164
+ this.readmeGenerator.updateNoteReadme(note);
3165
+ })
3166
+ )
3167
+ );
3168
+ for (const result of results) {
3169
+ if (result.status === "fulfilled") {
3170
+ successCount++;
3171
+ } else {
3172
+ failCount++;
3173
+ logger.error("\u66F4\u65B0\u7B14\u8BB0\u5931\u8D25", result.reason);
3174
+ }
3175
+ }
3176
+ }
3177
+ if (failCount > 0) {
3178
+ logger.warn(`\u66F4\u65B0\u5B8C\u6210\uFF1A\u6210\u529F ${successCount} \u7BC7\uFF0C\u5931\u8D25 ${failCount} \u7BC7`);
3179
+ }
3180
+ }
3181
+ /**
3182
+ * 更新侧边栏配置
3183
+ * @param notes - 笔记信息数组
3184
+ */
3185
+ async updateSidebar(notes) {
3186
+ if (!existsSync7(ROOT_README_PATH)) {
3187
+ logger.error("\u672A\u627E\u5230\u9996\u9875 README\uFF0C\u65E0\u6CD5\u751F\u6210\u4FA7\u8FB9\u680F");
3188
+ return;
3189
+ }
3190
+ const content = readFileSync4(ROOT_README_PATH, "utf-8");
3191
+ const lines = content.split("\n");
3192
+ const itemList = [];
3193
+ const titles = [];
3194
+ const titlesNotesCount = [];
3195
+ let currentNoteCount = 0;
3196
+ let inTocRegion = false;
3197
+ for (const line of lines) {
3198
+ if (line.includes("<!-- region:toc -->")) {
3199
+ inTocRegion = true;
3200
+ continue;
3201
+ }
3202
+ if (line.includes("<!-- endregion:toc -->")) {
3203
+ inTocRegion = false;
3204
+ continue;
3205
+ }
3206
+ if (inTocRegion) {
3207
+ continue;
3208
+ }
3209
+ const parsed = parseNoteLine(line);
3210
+ if (parsed.isMatch && parsed.noteIndex) {
3211
+ const note = notes.find((n) => n.index === parsed.noteIndex);
3212
+ if (!note) {
3213
+ logger.warn(`\u672A\u627E\u5230\u7B14\u8BB0\u7D22\u5F15: ${parsed.noteIndex}`);
3214
+ continue;
3215
+ }
3216
+ let statusEmoji = "\u23F0 ";
3217
+ if (note?.config) {
3218
+ if (note.config.done) {
3219
+ statusEmoji = "\u2705 ";
3220
+ }
3221
+ }
3222
+ const sidebarShowNoteId2 = this.configManager.get("sidebarShowNoteId");
3223
+ let displayText = note.dirName;
3224
+ if (!sidebarShowNoteId2) {
3225
+ displayText = note.dirName.replace(/^\d{4}\.\s/, "");
3226
+ }
3227
+ itemList.push({
3228
+ text: statusEmoji + displayText,
3229
+ link: `/notes/${note.dirName}/README`
3230
+ });
3231
+ currentNoteCount++;
3232
+ continue;
3233
+ }
3234
+ const titleMatch = line.match(/^(#{2,})\s+(.+)$/);
3235
+ if (titleMatch) {
3236
+ if (titles.length > 0) {
3237
+ titlesNotesCount.push(currentNoteCount);
3238
+ }
3239
+ titles.push(line);
3240
+ currentNoteCount = 0;
3241
+ }
3242
+ }
3243
+ if (titles.length > 0) {
3244
+ titlesNotesCount.push(currentNoteCount);
3245
+ }
3246
+ const sidebarIsCollapsed = true;
3247
+ const hierarchicalSidebar = genHierarchicalSidebar(
3248
+ itemList,
3249
+ titles,
3250
+ titlesNotesCount,
3251
+ sidebarIsCollapsed
3252
+ );
3253
+ writeFileSync3(
3254
+ VP_SIDEBAR_PATH,
3255
+ JSON.stringify(hierarchicalSidebar, null, 2),
3256
+ "utf-8"
3257
+ );
3258
+ logger.info("\u5DF2\u66F4\u65B0\u4FA7\u8FB9\u680F\u914D\u7F6E");
3259
+ }
3260
+ /**
3261
+ * 更新首页 README
3262
+ * @param notes - 笔记信息数组
3263
+ */
3264
+ async updateHomeReadme(notes) {
3265
+ this.readmeGenerator.updateHomeReadme(notes, ROOT_README_PATH);
3266
+ }
3267
+ /**
3268
+ * 增量更新首页 README 中的单个笔记
3269
+ * @param noteIndex - 笔记索引
3270
+ * @param updates - 需要更新的配置字段
3271
+ */
3272
+ async updateNoteInReadme(noteIndex, updates) {
3273
+ const item = this.noteIndexCache.getByNoteIndex(noteIndex);
3274
+ if (!item) {
3275
+ logger.warn(`\u5C1D\u8BD5\u66F4\u65B0\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0: ${noteIndex}`);
3276
+ return;
3277
+ }
3278
+ const content = await fsPromises2.readFile(ROOT_README_PATH, "utf-8");
3279
+ const lines = content.split("\n");
3280
+ const repoOwner = this.configManager.get("author");
3281
+ const repoName2 = this.configManager.get("repoName");
3282
+ const mergedConfig = { ...item.noteConfig, ...updates };
3283
+ const tempNoteInfo = {
3284
+ index: noteIndex,
3285
+ dirName: item.folderName,
3286
+ path: "",
3287
+ readmePath: "",
3288
+ configPath: "",
3289
+ config: mergedConfig
3290
+ };
3291
+ let updated = false;
3292
+ for (let i = 0; i < lines.length; i++) {
3293
+ const parsed = parseNoteLine(lines[i]);
3294
+ if (parsed.noteIndex === noteIndex) {
3295
+ lines[i] = buildNoteLineMarkdown(tempNoteInfo, repoOwner, repoName2);
3296
+ updated = true;
3297
+ }
3298
+ }
3299
+ if (updated) {
3300
+ await fsPromises2.writeFile(ROOT_README_PATH, lines.join("\n"), "utf-8");
3301
+ logger.info(`\u589E\u91CF\u66F4\u65B0 README.md \u4E2D\u7684\u7B14\u8BB0: ${noteIndex}`);
3302
+ } else {
3303
+ logger.warn(`README.md \u4E2D\u672A\u627E\u5230\u7B14\u8BB0: ${noteIndex}`);
3304
+ }
3305
+ }
3306
+ /**
3307
+ * 从首页 README 中删除笔记
3308
+ * @param noteIndex - 笔记索引
3309
+ */
3310
+ async deleteNoteFromReadme(noteIndex) {
3311
+ const content = await fsPromises2.readFile(ROOT_README_PATH, "utf-8");
3312
+ const lines = content.split("\n");
3313
+ const linesToRemove = [];
3314
+ for (let i = 0; i < lines.length; i++) {
3315
+ const parsed = parseNoteLine(lines[i]);
3316
+ if (parsed.noteIndex === noteIndex) {
3317
+ linesToRemove.push(i);
3318
+ }
3319
+ }
3320
+ if (linesToRemove.length > 0) {
3321
+ for (let i = linesToRemove.length - 1; i >= 0; i--) {
3322
+ lines.splice(linesToRemove[i], 1);
3323
+ }
3324
+ await fsPromises2.writeFile(ROOT_README_PATH, lines.join("\n"), "utf-8");
3325
+ logger.info(
3326
+ `\u4ECE README.md \u4E2D\u5220\u9664\u7B14\u8BB0: ${noteIndex} (${linesToRemove.length} \u5904\u5F15\u7528)`
3327
+ );
3328
+ } else {
3329
+ logger.warn(`README.md \u4E2D\u672A\u627E\u5230\u7B14\u8BB0: ${noteIndex}`);
3330
+ }
3331
+ }
3332
+ /**
3333
+ * 在首页 README 末尾添加新笔记
3334
+ * @param noteIndex - 笔记索引
3335
+ */
3336
+ async appendNoteToReadme(noteIndex) {
3337
+ const item = this.noteIndexCache.getByNoteIndex(noteIndex);
3338
+ if (!item) {
3339
+ logger.warn(`\u5C1D\u8BD5\u6DFB\u52A0\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0: ${noteIndex}`);
3340
+ return;
3341
+ }
3342
+ const content = await fsPromises2.readFile(ROOT_README_PATH, "utf-8");
3343
+ const lines = content.split("\n");
3344
+ const repoOwner = this.configManager.get("author");
3345
+ const repoName2 = this.configManager.get("repoName");
3346
+ const tempNoteInfo = {
3347
+ index: noteIndex,
3348
+ dirName: item.folderName,
3349
+ path: "",
3350
+ readmePath: "",
3351
+ configPath: "",
3352
+ config: item.noteConfig
3353
+ };
3354
+ const noteLine = buildNoteLineMarkdown(tempNoteInfo, repoOwner, repoName2);
3355
+ lines.push(noteLine);
3356
+ await fsPromises2.writeFile(ROOT_README_PATH, lines.join("\n"), "utf-8");
3357
+ logger.info(`\u5728 README.md \u672B\u5C3E\u6DFB\u52A0\u7B14\u8BB0: ${noteIndex}`);
3358
+ }
3359
+ /**
3360
+ * 重新生成 sidebar.json(基于当前 README.md)
3361
+ * @param notes - 可选的笔记列表,不传则内部扫描
3362
+ */
3363
+ async regenerateSidebar(notes) {
3364
+ const allNotes = notes ?? (this.noteIndexCache.isInitialized() ? this.noteIndexCache.toNoteInfoList() : this.noteManager.scanNotes());
3365
+ await this.updateSidebar(allNotes);
3366
+ logger.info("\u91CD\u65B0\u751F\u6210 sidebar.json");
3367
+ }
3368
+ };
3369
+
3370
+ // services/note/service.ts
3371
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync5 } from "fs";
3372
+ import { join as join8 } from "path";
3373
+ import { v4 as uuidv4 } from "uuid";
3374
+
3375
+ // config/templates.ts
3376
+ function generateNoteTitle(noteIndex, title, repoUrl) {
3377
+ const dirName = `${noteIndex}. ${title}`;
3378
+ const encodedDirName = encodeURIComponent(dirName);
3379
+ return `# [${dirName}](${repoUrl}/${encodedDirName})`;
3380
+ }
3381
+
3382
+ // services/note/service.ts
3383
+ var NEW_NOTES_README_MD_TEMPLATE = `
3384
+ <!-- region:toc -->
3385
+
3386
+ - [1. \u{1F3AF} \u672C\u8282\u5185\u5BB9](#1--\u672C\u8282\u5185\u5BB9)
3387
+ - [2. \u{1FAE7} \u8BC4\u4EF7](#2--\u8BC4\u4EF7)
3388
+
3389
+ <!-- endregion:toc -->
3390
+
3391
+ ## 1. \u{1F3AF} \u672C\u8282\u5185\u5BB9
3392
+
3393
+ - todo
3394
+
3395
+ ## 2. \u{1FAE7} \u8BC4\u4EF7
3396
+
3397
+ - todo
3398
+ `;
3399
+ var NoteService = class _NoteService {
3400
+ static instance;
3401
+ noteManager;
3402
+ noteIndexCache;
3403
+ ignoredConfigPaths = /* @__PURE__ */ new Set();
3404
+ constructor() {
3405
+ this.noteManager = NoteManager.getInstance();
3406
+ this.noteIndexCache = NoteIndexCache.getInstance();
3407
+ }
3408
+ static getInstance() {
3409
+ if (!_NoteService.instance) {
3410
+ _NoteService.instance = new _NoteService();
3411
+ }
3412
+ return _NoteService.instance;
3413
+ }
3414
+ /**
3415
+ * 标记配置文件在下次变更时被忽略(防止 API 写入触发文件监听循环)
3416
+ * @param configPath - 配置文件路径
3417
+ */
3418
+ ignoreNextConfigChange(configPath) {
3419
+ this.ignoredConfigPaths.add(configPath);
3420
+ }
3421
+ /**
3422
+ * 检查配置文件是否应该被忽略
3423
+ * @param configPath - 配置文件路径
3424
+ * @returns 是否应该忽略
3425
+ */
3426
+ shouldIgnoreConfigChange(configPath) {
3427
+ if (this.ignoredConfigPaths.has(configPath)) {
3428
+ this.ignoredConfigPaths.delete(configPath);
3429
+ return true;
3430
+ }
3431
+ return false;
3432
+ }
3433
+ /**
3434
+ * 获取所有笔记
3435
+ * dev 模式下(缓存已初始化)从内存读取,其他模式回退到文件扫描
3436
+ * @returns 笔记信息数组
3437
+ */
3438
+ getAllNotes() {
3439
+ if (this.noteIndexCache.isInitialized()) {
3440
+ return this.noteIndexCache.toNoteInfoList();
3441
+ }
3442
+ return this.noteManager.scanNotes();
3443
+ }
3444
+ /**
3445
+ * 获取笔记(通过索引)
3446
+ * @param noteIndex - 笔记索引(文件夹前 4 位数字)
3447
+ * @returns 笔记信息,未找到时返回 undefined
3448
+ */
3449
+ getNoteByIndex(noteIndex) {
3450
+ return this.noteManager.getNoteByIndex(noteIndex);
3451
+ }
3452
+ /**
3453
+ * 创建新笔记
3454
+ * @param options - 创建选项
3455
+ * @returns 新创建的笔记信息
3456
+ */
3457
+ async createNote(options = {}) {
3458
+ const {
3459
+ title = "new",
3460
+ category,
3461
+ enableDiscussions = false,
3462
+ configId,
3463
+ usedIndexes
3464
+ } = options;
3465
+ const noteIndex = this.generateNextNoteIndex(usedIndexes);
3466
+ const dirName = `${noteIndex}. ${title}`;
3467
+ const notePath = join8(NOTES_PATH, dirName);
3468
+ await ensureDirectory(notePath);
3469
+ const readmePath = join8(notePath, "README.md");
3470
+ const noteTitle = generateNoteTitle(noteIndex, title, REPO_NOTES_URL);
3471
+ const readmeContent = noteTitle + "\n" + NEW_NOTES_README_MD_TEMPLATE;
3472
+ writeFileSync4(readmePath, readmeContent, "utf-8");
3473
+ const configPath = join8(notePath, ".tnotes.json");
3474
+ const now = Date.now();
3475
+ const config2 = {
3476
+ id: configId || uuidv4(),
3477
+ // 配置 ID 使用 UUID(跨知识库唯一)
3478
+ bilibili: [],
3479
+ tnotes: [],
3480
+ yuque: [],
3481
+ done: false,
3482
+ category,
3483
+ enableDiscussions,
3484
+ created_at: now,
3485
+ updated_at: now
3486
+ };
3487
+ this.noteManager.writeNoteConfig(configPath, config2);
3488
+ logger.info(`Created new note: ${dirName}`);
3489
+ return {
3490
+ index: noteIndex,
3491
+ // 返回的 id 是笔记索引(目录前缀)
3492
+ path: notePath,
3493
+ dirName,
3494
+ readmePath,
3495
+ configPath,
3496
+ config: config2
3497
+ };
3498
+ }
3499
+ /**
3500
+ * 生成下一个笔记索引(填充空缺)
3501
+ * @param usedIndexes - 可选的已使用编号集合,不传则内部扫描
3502
+ * @returns 新的笔记索引(4位数字字符串,从 0001 到 9999)
3503
+ */
3504
+ generateNextNoteIndex(usedIndexes) {
3505
+ if (!usedIndexes) {
3506
+ const notes = this.getAllNotes();
3507
+ usedIndexes = /* @__PURE__ */ new Set();
3508
+ for (const note of notes) {
3509
+ const id = parseInt(note.index, 10);
3510
+ if (!isNaN(id) && id >= 1 && id <= 9999) {
3511
+ usedIndexes.add(id);
3512
+ }
3513
+ }
3514
+ }
3515
+ if (usedIndexes.size === 0) {
3516
+ return "0001";
3517
+ }
3518
+ for (let i = 1; i <= 9999; i++) {
3519
+ if (!usedIndexes.has(i)) {
3520
+ return i.toString().padStart(CONSTANTS.NOTE_INDEX_LENGTH, "0");
3521
+ }
3522
+ }
3523
+ throw new Error("\u6240\u6709\u7B14\u8BB0\u7F16\u53F7 (0001-9999) \u5DF2\u88AB\u5360\u7528\uFF0C\u65E0\u6CD5\u521B\u5EFA\u65B0\u7B14\u8BB0");
3524
+ }
3525
+ /**
3526
+ * 更新笔记配置
3527
+ * @param noteIndex - 笔记索引
3528
+ * @param updates - 配置更新
3529
+ */
3530
+ async updateNoteConfig(noteIndex, updates) {
3531
+ const note = this.getNoteByIndex(noteIndex);
3532
+ if (!note || !note.config) {
3533
+ throw new Error(`Note not found or no config: ${noteIndex}`);
3534
+ }
3535
+ const oldConfig = { ...note.config };
3536
+ const updatedConfig = {
3537
+ ...note.config,
3538
+ ...updates,
3539
+ updated_at: Date.now()
3540
+ };
3541
+ this.ignoreNextConfigChange(note.configPath);
3542
+ this.noteManager.updateNoteConfig(note, updatedConfig);
3543
+ this.noteIndexCache.updateConfig(noteIndex, updatedConfig);
3544
+ const needsGlobalUpdate = this.checkNeedsGlobalUpdate(
3545
+ oldConfig,
3546
+ updatedConfig
3547
+ );
3548
+ if (needsGlobalUpdate) {
3549
+ logger.info(`\u68C0\u6D4B\u5230\u5168\u5C40\u5B57\u6BB5\u53D8\u66F4 (${noteIndex})\uFF0C\u6B63\u5728\u589E\u91CF\u66F4\u65B0\u5168\u5C40\u6587\u4EF6...`);
3550
+ const readmeService = ReadmeService.getInstance();
3551
+ await readmeService.updateNoteInReadme(noteIndex, updates);
3552
+ await readmeService.regenerateSidebar();
3553
+ logger.info(`\u5168\u5C40\u6587\u4EF6\u589E\u91CF\u66F4\u65B0\u5B8C\u6210 (${noteIndex})`);
3554
+ } else {
3555
+ logger.debug(`\u914D\u7F6E\u66F4\u65B0\u4E0D\u5F71\u54CD\u5168\u5C40\u6587\u4EF6 (${noteIndex})`);
3556
+ }
3557
+ }
3558
+ /**
3559
+ * 检查配置更新是否需要触发全局更新
3560
+ * @param oldConfig - 旧配置
3561
+ * @param newConfig - 新配置
3562
+ * @returns 是否需要全局更新
3563
+ */
3564
+ checkNeedsGlobalUpdate(oldConfig, newConfig) {
3565
+ const globalFields = ["done"];
3566
+ for (const field of globalFields) {
3567
+ if (oldConfig[field] !== newConfig[field]) {
3568
+ return true;
3569
+ }
3570
+ }
3571
+ return false;
3572
+ }
3573
+ /**
3574
+ * 修正笔记标题
3575
+ * @param noteInfo - 笔记信息
3576
+ * @returns 是否进行了修正
3577
+ */
3578
+ async fixNoteTitle(noteInfo) {
3579
+ try {
3580
+ const readmeContent = readFileSync5(noteInfo.readmePath, "utf-8");
3581
+ if (readmeContent.length === 0) return false;
3582
+ const lines = readmeContent.split("\n");
3583
+ const match = noteInfo.dirName.match(/^\d{4}\.\s+(.+)$/);
3584
+ if (!match) {
3585
+ logger.warn(`\u68C0\u6D4B\u5230\u9519\u8BEF\u7684\u7B14\u8BB0\u76EE\u5F55\u540D\u79F0\uFF1A${noteInfo.dirName}`);
3586
+ return false;
3587
+ }
3588
+ const expectedTitle = match[1];
3589
+ const expectedH1 = generateNoteTitle(
3590
+ noteInfo.index,
3591
+ expectedTitle,
3592
+ REPO_NOTES_URL
3593
+ );
3594
+ const firstLine = lines[0].trim();
3595
+ if (!firstLine.startsWith("# ")) {
3596
+ lines.unshift(expectedH1);
3597
+ writeFileSync4(noteInfo.readmePath, lines.join("\n"), "utf-8");
3598
+ logger.info(`Added title to: ${noteInfo.dirName}`);
3599
+ return true;
3600
+ }
3601
+ if (firstLine !== expectedH1) {
3602
+ lines[0] = expectedH1;
3603
+ writeFileSync4(noteInfo.readmePath, lines.join("\n"), "utf-8");
3604
+ logger.info(`Fixed title for: ${noteInfo.dirName}`);
3605
+ return true;
3606
+ }
3607
+ return false;
3608
+ } catch (error) {
3609
+ logger.error(`Failed to fix title for: ${noteInfo.dirName}`, error);
3610
+ return false;
3611
+ }
3612
+ }
3613
+ /**
3614
+ * 修正所有笔记的标题
3615
+ * @param providedNotes - 可选的笔记列表,不传则内部扫描
3616
+ * @returns 修正的笔记数量
3617
+ */
3618
+ async fixAllNoteTitles(providedNotes) {
3619
+ const notes = providedNotes ?? this.getAllNotes();
3620
+ let fixedCount = 0;
3621
+ for (const note of notes) {
3622
+ const fixed = await this.fixNoteTitle(note);
3623
+ if (fixed) {
3624
+ fixedCount++;
3625
+ }
3626
+ }
3627
+ if (fixedCount > 0) {
3628
+ logger.info(`Fixed ${fixedCount} note titles`);
3629
+ }
3630
+ return fixedCount;
3631
+ }
3632
+ };
3633
+
3634
+ // services/file-watcher/service.ts
3635
+ var NOTES_DIR_NOT_SET_ERROR = "NOTES_DIR_PATH \u672A\u8BBE\u7F6E\uFF0C\u65E0\u6CD5\u542F\u52A8\u6587\u4EF6\u76D1\u542C";
3636
+ var UPDATE_UNLOCK_DELAY_MS2 = 500;
3637
+ var FileWatcherService = class {
3638
+ constructor(notesDir = NOTES_DIR_PATH) {
3639
+ this.notesDir = notesDir;
3640
+ if (!this.notesDir) {
3641
+ throw new Error(NOTES_DIR_NOT_SET_ERROR);
3642
+ }
3643
+ this.init();
3644
+ }
3645
+ watchState;
3646
+ scheduler;
3647
+ renameDetector;
3648
+ configHandler;
3649
+ readmeHandler;
3650
+ coordinator;
3651
+ folderHandler;
3652
+ adapter;
3653
+ noteService;
3654
+ readmeService;
3655
+ noteIndexCache;
3656
+ unlockTimer = null;
3657
+ init() {
3658
+ this.noteService = NoteService.getInstance();
3659
+ this.readmeService = ReadmeService.getInstance();
3660
+ this.noteIndexCache = NoteIndexCache.getInstance();
3661
+ this.watchState = this.initWatchState();
3662
+ this.scheduler = this.initScheduler();
3663
+ this.folderHandler = this.initFolderHandler();
3664
+ this.renameDetector = this.initRenameDetector();
3665
+ this.configHandler = this.initConfigHandler();
3666
+ this.readmeHandler = this.initReadmeHandler();
3667
+ this.coordinator = this.initCoordinator();
3668
+ this.adapter = this.initAdapter();
3669
+ }
3670
+ initWatchState() {
3671
+ const watchState = new WatchState({ notesDir: this.notesDir, logger });
3672
+ watchState.initializeFromDisk();
3673
+ return watchState;
3674
+ }
3675
+ initScheduler() {
3676
+ return new EventScheduler({
3677
+ onFlush: (events) => this.handleFileChange(events),
3678
+ onPauseForBatch: () => logger.warn("\u76D1\u542C\u670D\u52A1\u6682\u505C 3s \u7B49\u5F85\u6279\u91CF\u66F4\u65B0\u5B8C\u6210..."),
3679
+ onResumeAfterBatch: () => logger.info("\u6062\u590D\u81EA\u52A8\u76D1\u542C"),
3680
+ reinit: () => this.watchState.initializeFromDisk()
3681
+ });
3682
+ }
3683
+ initFolderHandler() {
3684
+ return new FolderChangeHandler({
3685
+ notesDir: this.notesDir,
3686
+ watchState: this.watchState,
3687
+ scheduler: this.scheduler,
3688
+ noteService: this.noteService,
3689
+ readmeService: this.readmeService,
3690
+ noteIndexCache: this.noteIndexCache,
3691
+ logger
3692
+ });
3693
+ }
3694
+ initRenameDetector() {
3695
+ return new RenameDetector({
3696
+ notesDir: this.notesDir,
3697
+ dirCache: {
3698
+ has: (name) => this.watchState.hasNoteDir(name),
3699
+ add: (name) => this.watchState.addNoteDir(name),
3700
+ delete: (name) => this.watchState.deleteNoteDir(name)
3701
+ },
3702
+ logger,
3703
+ onDelete: (oldName) => this.folderHandler.handleFolderDeletion(oldName),
3704
+ onRename: (oldName, newName) => this.folderHandler.handleFolderRenameUpdate(oldName, newName)
3705
+ });
3706
+ }
3707
+ initConfigHandler() {
3708
+ return new ConfigChangeHandler({
3709
+ state: this.watchState,
3710
+ noteService: this.noteService,
3711
+ noteIndexCache: this.noteIndexCache,
3712
+ logger
3713
+ });
3714
+ }
3715
+ initReadmeHandler() {
3716
+ return new ReadmeChangeHandler({ noteService: this.noteService });
3717
+ }
3718
+ initCoordinator() {
3719
+ return new GlobalUpdateCoordinator({
3720
+ readmeService: this.readmeService,
3721
+ noteIndexCache: this.noteIndexCache,
3722
+ logger
3723
+ });
3724
+ }
3725
+ initAdapter() {
3726
+ return new FsWatcherAdapter({
3727
+ notesDir: this.notesDir,
3728
+ isUpdating: () => this.scheduler.getUpdating(),
3729
+ onRename: (folderName) => this.renameDetector.handleFsRename(folderName),
3730
+ onNoteEvent: (event) => this.onNoteEvent(event),
3731
+ logger
3732
+ });
3733
+ }
3734
+ start() {
3735
+ this.watchState.initializeFromDisk();
3736
+ this.adapter.start();
3737
+ }
3738
+ stop() {
3739
+ this.adapter.stop();
3740
+ this.scheduler.clearTimers();
3741
+ this.renameDetector.clearTimers();
3742
+ this.folderHandler.clearTimers();
3743
+ if (this.unlockTimer) {
3744
+ clearTimeout(this.unlockTimer);
3745
+ this.unlockTimer = null;
3746
+ }
3747
+ logger.info("\u6587\u4EF6\u76D1\u542C\u670D\u52A1\u5DF2\u505C\u6B62");
3748
+ }
3749
+ pause() {
3750
+ this.scheduler.setUpdating(true);
3751
+ logger.info("\u6587\u4EF6\u76D1\u542C\u5DF2\u6682\u505C");
3752
+ }
3753
+ resume() {
3754
+ this.watchState.initializeFromDisk();
3755
+ this.scheduler.setUpdating(false);
3756
+ logger.info("\u6587\u4EF6\u76D1\u542C\u5DF2\u6062\u590D");
3757
+ }
3758
+ isWatching() {
3759
+ return this.adapter.isWatching();
3760
+ }
3761
+ // #region - 私有实现
3762
+ onNoteEvent(event) {
3763
+ if (!this.isNoteFile(event.path)) return;
3764
+ if (!this.watchState.updateFileHash(event.path)) return;
3765
+ if (this.scheduler.recordChangeAndDetectBatch()) return;
3766
+ this.scheduler.enqueue(event);
3767
+ }
3768
+ async handleFileChange(events) {
3769
+ try {
3770
+ const configChanges = events.filter(
3771
+ (e) => e.type === WATCH_EVENT_TYPES.CONFIG
3772
+ );
3773
+ const readmeChanges = events.filter(
3774
+ (e) => e.type === WATCH_EVENT_TYPES.README
3775
+ );
3776
+ const changedNoteIndexes = await this.configHandler.handle(configChanges);
3777
+ if (changedNoteIndexes.length > 0) {
3778
+ await safeExecute(
3779
+ "\u914D\u7F6E\u53D8\u66F4\u66F4\u65B0",
3780
+ () => this.coordinator.applyConfigUpdates(changedNoteIndexes),
3781
+ logger
3782
+ );
3783
+ return;
3784
+ }
3785
+ await safeExecute(
3786
+ "README \u53D8\u66F4\u66F4\u65B0",
3787
+ async () => {
3788
+ await this.readmeHandler.handle(readmeChanges);
3789
+ await this.coordinator.updateNoteReadmesOnly(events);
3790
+ },
3791
+ logger
3792
+ );
3793
+ } finally {
3794
+ if (this.unlockTimer) clearTimeout(this.unlockTimer);
3795
+ this.unlockTimer = setTimeout(() => {
3796
+ this.unlockTimer = null;
3797
+ this.scheduler.setUpdating(false);
3798
+ }, UPDATE_UNLOCK_DELAY_MS2);
3799
+ }
3800
+ }
3801
+ isNoteFile(filePath) {
3802
+ return filePath.endsWith("README.md") || filePath.endsWith(".tnotes.json");
3803
+ }
3804
+ // #endregion - 私有实现
3805
+ };
3806
+
3807
+ // services/git/service.ts
3808
+ var GitService = class {
3809
+ gitManager;
3810
+ constructor() {
3811
+ this.gitManager = new GitManager(ROOT_DIR_PATH);
3812
+ }
3813
+ /**
3814
+ * 推送到远程仓库
3815
+ * @param options - 推送选项
3816
+ */
3817
+ async push(options = {}) {
3818
+ const { message, branch, force = false } = options;
3819
+ logger.info("Pushing to remote repository...");
3820
+ if (message) {
3821
+ await this.gitManager.pushWithCommit(message, { force });
3822
+ } else {
3823
+ await this.gitManager.push({ setUpstream: !!branch, force });
3824
+ }
3825
+ logger.info("Push completed successfully");
3826
+ }
3827
+ /**
3828
+ * 从远程仓库拉取
3829
+ * @param options - 拉取选项
3830
+ */
3831
+ async pull(options = {}) {
3832
+ const { rebase = false } = options;
3833
+ logger.info("Pulling from remote repository...");
3834
+ await this.gitManager.pull({ rebase });
3835
+ logger.info("Pull completed successfully");
3836
+ }
3837
+ /**
3838
+ * 同步本地和远程仓库(先拉取后推送)
3839
+ * @param commitMessage - 可选的提交信息
3840
+ */
3841
+ async sync(commitMessage) {
3842
+ logger.info("Syncing with remote repository...");
3843
+ await this.gitManager.sync({ commitMessage });
3844
+ logger.info("Sync completed successfully");
3845
+ }
3846
+ /**
3847
+ * 获取 Git 状态
3848
+ * @returns Git 状态信息
3849
+ */
3850
+ async getStatus() {
3851
+ return await this.gitManager.getStatus();
3852
+ }
3853
+ /**
3854
+ * 检查是否有未提交的更改
3855
+ * @returns 是否有未提交的更改
3856
+ */
3857
+ async hasChanges() {
3858
+ const status = await this.getStatus();
3859
+ return status.hasChanges;
3860
+ }
3861
+ /**
3862
+ * 生成自动提交信息
3863
+ * @returns 自动生成的提交信息
3864
+ */
3865
+ generateCommitMessage() {
3866
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
3867
+ const time = (/* @__PURE__ */ new Date()).toTimeString().split(" ")[0];
3868
+ return `\u{1F4DD} Update notes - ${date} ${time}`;
3869
+ }
3870
+ /**
3871
+ * 快速提交并推送(使用自动生成的提交信息)
3872
+ * @param options - 推送选项
3873
+ */
3874
+ async quickPush(options = {}) {
3875
+ if (!options.skipCheck && !await this.hasChanges()) {
3876
+ logger.info("No changes to commit");
3877
+ return;
3878
+ }
3879
+ const message = this.generateCommitMessage();
3880
+ await this.push({ message, force: options.force });
3881
+ }
3882
+ };
3883
+
3884
+ // services/sync-core/service.ts
3885
+ import { existsSync as existsSync8 } from "fs";
3886
+ import { join as join9, basename as basename2 } from "path";
3887
+ var SyncCoreService = class {
3888
+ /**
3889
+ * 同步单个仓库的 submodule 到最新版本
3890
+ */
3891
+ async syncSingleRepo(targetDir) {
3892
+ const repoName2 = basename2(targetDir);
3893
+ const submodulePath = join9(targetDir, ".vitepress", "tnotes");
3894
+ try {
3895
+ if (!existsSync8(join9(targetDir, ".gitmodules"))) {
3896
+ return {
3897
+ dir: targetDir,
3898
+ repoName: repoName2,
3899
+ success: false,
3900
+ updated: false,
3901
+ error: "\u672A\u627E\u5230 .gitmodules\uFF0C\u8BE5\u4ED3\u5E93\u672A\u914D\u7F6E submodule"
3902
+ };
3903
+ }
3904
+ if (!existsSync8(submodulePath)) {
3905
+ await runCommand("git submodule update --init", targetDir);
3906
+ }
3907
+ const beforeHash = (await runCommand("git rev-parse HEAD", submodulePath)).trim();
3908
+ const beforeTime = (await runCommand("git log -1 --format=%ci HEAD", submodulePath)).trim().replace(/ [+-]\d{4}$/, "");
3909
+ await runCommand("git fetch origin", submodulePath);
3910
+ await runCommand("git reset --hard origin/main", submodulePath);
3911
+ const afterHash = (await runCommand("git rev-parse HEAD", submodulePath)).trim();
3912
+ const afterTime = (await runCommand("git log -1 --format=%ci HEAD", submodulePath)).trim().replace(/ [+-]\d{4}$/, "");
3913
+ const updated = beforeHash !== afterHash;
3914
+ if (updated) {
3915
+ await runCommand("git add .vitepress/tnotes", targetDir);
3916
+ await runCommand(
3917
+ 'git commit -m "chore: update tnotesjs/core"',
3918
+ targetDir
3919
+ );
3920
+ }
3921
+ return {
3922
+ dir: targetDir,
3923
+ repoName: repoName2,
3924
+ success: true,
3925
+ updated,
3926
+ beforeHash: beforeHash.substring(0, 7),
3927
+ beforeTime,
3928
+ afterHash: afterHash.substring(0, 7),
3929
+ afterTime
3930
+ };
3931
+ } catch (error) {
3932
+ const errorMessage = error instanceof Error ? error.message : String(error);
3933
+ return {
3934
+ dir: targetDir,
3935
+ repoName: repoName2,
3936
+ success: false,
3937
+ updated: false,
3938
+ error: errorMessage
3939
+ };
3940
+ }
3941
+ }
3942
+ /**
3943
+ * 同步所有兄弟仓库的 tnotesjs/core 到最新版本
3944
+ */
3945
+ async syncToAllRepos() {
3946
+ try {
3947
+ const targetDirs = getTargetDirs(TNOTES_BASE_DIR, "TNotes.", [
3948
+ ROOT_DIR_PATH,
3949
+ TNOTES_CORE_DIR,
3950
+ EN_WORDS_DIR
3951
+ ]);
3952
+ if (targetDirs.length === 0) {
3953
+ logger.warn("\u672A\u627E\u5230\u7B26\u5408\u6761\u4EF6\u7684\u76EE\u6807\u76EE\u5F55");
3954
+ return;
3955
+ }
3956
+ logger.info(`\u6B63\u5728\u540C\u6B65 ${targetDirs.length} \u4E2A\u4ED3\u5E93\u7684 tnotesjs/core...`);
3957
+ console.log();
3958
+ const results = [];
3959
+ for (let i = 0; i < targetDirs.length; i++) {
3960
+ const dir = targetDirs[i];
3961
+ const repoName2 = basename2(dir);
3962
+ logger.info(`[${i + 1}/${targetDirs.length}] ${repoName2}`);
3963
+ const result = await this.syncSingleRepo(dir);
3964
+ results.push(result);
3965
+ if (result.success) {
3966
+ if (result.updated) {
3967
+ logger.success(
3968
+ ` \u2713 \u5DF2\u66F4\u65B0 ${result.beforeHash}(${result.beforeTime}) \u2192 ${result.afterHash}(${result.afterTime})
3969
+ `
3970
+ );
3971
+ } else {
3972
+ logger.info(
3973
+ ` - \u5DF2\u662F\u6700\u65B0 ${result.afterHash}(${result.afterTime})
3974
+ `
3975
+ );
3976
+ }
3977
+ } else {
3978
+ logger.error(` \u2717 \u5931\u8D25: ${result.error}
3979
+ `);
3980
+ }
3981
+ }
3982
+ const successCount = results.filter((r) => r.success).length;
3983
+ const updatedCount = results.filter((r) => r.updated).length;
3984
+ const failCount = results.length - successCount;
3985
+ console.log("\u2501".repeat(50));
3986
+ if (failCount === 0) {
3987
+ logger.success(
3988
+ `\u2728 \u540C\u6B65\u5B8C\u6210: ${updatedCount} \u4E2A\u4ED3\u5E93\u5DF2\u66F4\u65B0, ${successCount - updatedCount} \u4E2A\u5DF2\u662F\u6700\u65B0 (\u5171 ${results.length} \u4E2A)`
3989
+ );
3990
+ } else {
3991
+ logger.warn(
3992
+ `\u26A0\uFE0F \u540C\u6B65\u5B8C\u6210: ${successCount} \u6210\u529F (${updatedCount} \u66F4\u65B0), ${failCount} \u5931\u8D25 (\u5171 ${results.length} \u4E2A)`
3993
+ );
3994
+ console.log("\n\u5931\u8D25\u7684\u4ED3\u5E93:");
3995
+ results.filter((r) => !r.success).forEach((r, index) => {
3996
+ console.log(` ${index + 1}. ${r.repoName}`);
3997
+ console.log(` \u9519\u8BEF: ${r.error}`);
3998
+ });
3999
+ }
4000
+ } catch (error) {
4001
+ const errorMessage = error instanceof Error ? error.message : String(error);
4002
+ logger.error(`tnotesjs/core \u540C\u6B65\u5931\u8D25: ${errorMessage}`);
4003
+ throw error;
4004
+ }
4005
+ }
4006
+ };
4007
+
4008
+ // services/timestamp/service.ts
4009
+ import {
4010
+ existsSync as existsSync9,
4011
+ readFileSync as readFileSync6,
4012
+ writeFileSync as writeFileSync5,
4013
+ readdirSync as readdirSync4,
4014
+ statSync as statSync2
4015
+ } from "fs";
4016
+ import { join as join10 } from "path";
4017
+ import { execSync as execSync3 } from "child_process";
4018
+ var BIRTH_DATE = (/* @__PURE__ */ new Date("1999-06-29T00:00:00+08:00")).getTime();
4019
+ var TimestampService = class {
4020
+ noteManager;
4021
+ constructor() {
4022
+ this.noteManager = NoteManager.getInstance();
4023
+ }
4024
+ /**
4025
+ * 从 git 获取文件的创建时间和最后修改时间
4026
+ * @param noteDirPath - 笔记目录路径
4027
+ * @returns 时间戳对象,包含 created_at 和 updated_at
4028
+ */
4029
+ getGitTimestamps(noteDirPath) {
4030
+ try {
4031
+ const readmePath = join10(noteDirPath, "README.md");
4032
+ const createdAtOutput = execSync3(
4033
+ `git log --diff-filter=A --follow --format=%ct -- "${readmePath}"`,
4034
+ {
4035
+ cwd: ROOT_DIR_PATH,
4036
+ encoding: "utf-8",
4037
+ stdio: ["pipe", "pipe", "ignore"]
4038
+ }
4039
+ ).split(/\r?\n/).filter(Boolean).pop();
4040
+ const updatedAtOutput = execSync3(
4041
+ `git log -1 --format=%ct -- "${readmePath}"`,
4042
+ {
4043
+ cwd: ROOT_DIR_PATH,
4044
+ encoding: "utf-8",
4045
+ stdio: ["pipe", "pipe", "ignore"]
4046
+ }
4047
+ ).trim();
4048
+ if (!createdAtOutput || !updatedAtOutput) {
4049
+ return null;
4050
+ }
4051
+ return {
4052
+ created_at: parseInt(createdAtOutput) * 1e3,
4053
+ // 转换为毫秒
4054
+ updated_at: parseInt(updatedAtOutput) * 1e3
4055
+ };
4056
+ } catch (error) {
4057
+ logger.debug?.(`getGitTimestamps failed: ${noteDirPath}`, error);
4058
+ return null;
4059
+ }
4060
+ }
4061
+ /**
4062
+ * 修复单个笔记的时间戳
4063
+ * @param noteDir - 笔记目录名
4064
+ * @param forceUpdate - 是否强制更新(忽略现有值)
4065
+ * @returns 是否进行了修复
4066
+ */
4067
+ fixNoteTimestamps(noteDir, forceUpdate = false) {
4068
+ const configPath = join10(NOTES_DIR_PATH, noteDir, ".tnotes.json");
4069
+ if (!existsSync9(configPath)) {
4070
+ return false;
4071
+ }
4072
+ try {
4073
+ const configContent = readFileSync6(configPath, "utf-8");
4074
+ const config2 = JSON.parse(configContent);
4075
+ const noteDirPath = join10(NOTES_DIR_PATH, noteDir);
4076
+ const timestamps = this.getGitTimestamps(noteDirPath);
4077
+ if (!timestamps) {
4078
+ return false;
4079
+ }
4080
+ let modified = false;
4081
+ if (forceUpdate || !config2.created_at || config2.created_at !== timestamps.created_at) {
4082
+ config2.created_at = timestamps.created_at;
4083
+ modified = true;
4084
+ }
4085
+ if (forceUpdate) {
4086
+ if (config2.updated_at !== timestamps.updated_at) {
4087
+ config2.updated_at = timestamps.updated_at;
4088
+ modified = true;
4089
+ }
4090
+ } else {
4091
+ if (!config2.updated_at) {
4092
+ config2.updated_at = timestamps.updated_at;
4093
+ modified = true;
4094
+ } else if (timestamps.updated_at > config2.updated_at) {
4095
+ config2.updated_at = timestamps.updated_at;
4096
+ modified = true;
4097
+ }
4098
+ }
4099
+ if (modified) {
4100
+ this.noteManager.writeNoteConfig(configPath, config2);
4101
+ return true;
4102
+ }
4103
+ return false;
4104
+ } catch (error) {
4105
+ logger.error(`\u4FEE\u590D\u65F6\u95F4\u6233\u5931\u8D25: ${noteDir}`, error);
4106
+ return false;
4107
+ }
4108
+ }
4109
+ /**
4110
+ * 修复根配置文件的时间戳
4111
+ * @param forceUpdate - 是否强制更新
4112
+ * @returns 是否进行了修复
4113
+ */
4114
+ fixRootConfigTimestamps(forceUpdate = false) {
4115
+ try {
4116
+ const configContent = readFileSync6(ROOT_CONFIG_PATH, "utf-8");
4117
+ const config2 = JSON.parse(configContent);
4118
+ const createdAtOutput = execSync3("git log --reverse --format=%ct", {
4119
+ cwd: ROOT_DIR_PATH,
4120
+ encoding: "utf-8",
4121
+ stdio: ["pipe", "pipe", "ignore"]
4122
+ }).trim();
4123
+ const updatedAtOutput = execSync3("git log -1 --format=%ct", {
4124
+ cwd: ROOT_DIR_PATH,
4125
+ encoding: "utf-8",
4126
+ stdio: ["pipe", "pipe", "ignore"]
4127
+ }).trim();
4128
+ if (!createdAtOutput || !updatedAtOutput) {
4129
+ return false;
4130
+ }
4131
+ const firstTimestamp = createdAtOutput.split("\n")[0];
4132
+ const createdAt = parseInt(firstTimestamp) * 1e3;
4133
+ const updatedAt = parseInt(updatedAtOutput) * 1e3;
4134
+ let modified = false;
4135
+ if (forceUpdate || !config2.root_item.created_at || config2.root_item.created_at !== createdAt) {
4136
+ config2.root_item.created_at = createdAt;
4137
+ modified = true;
4138
+ }
4139
+ if (forceUpdate || !config2.root_item.updated_at || config2.root_item.updated_at !== updatedAt) {
4140
+ config2.root_item.updated_at = updatedAt;
4141
+ modified = true;
4142
+ }
4143
+ if (modified) {
4144
+ const daysSinceBirth = Math.floor((updatedAt - BIRTH_DATE) / (1e3 * 60 * 60 * 24)) + 1;
4145
+ config2.root_item.days_since_birth = daysSinceBirth;
4146
+ writeFileSync5(
4147
+ ROOT_CONFIG_PATH,
4148
+ JSON.stringify(config2, null, 2) + "\n",
4149
+ "utf-8"
4150
+ );
4151
+ return true;
4152
+ }
4153
+ return false;
4154
+ } catch (error) {
4155
+ logger.error("\u4FEE\u590D\u6839\u914D\u7F6E\u6587\u4EF6\u65F6\u95F4\u6233\u5931\u8D25", error);
4156
+ return false;
4157
+ }
4158
+ }
4159
+ /**
4160
+ * 修复所有笔记的时间戳
4161
+ * @param forceUpdate - 是否强制更新(忽略现有值,用于修复历史错误数据)
4162
+ * @returns 修复统计信息
4163
+ */
4164
+ async fixAllTimestamps(forceUpdate = false) {
4165
+ if (forceUpdate) {
4166
+ logger.info("\u6B63\u5728\u5F3A\u5236\u4FEE\u590D\u7B14\u8BB0\u65F6\u95F4\u6233\uFF08\u4F7F\u7528 git \u771F\u5B9E\u65F6\u95F4\uFF09...");
4167
+ } else {
4168
+ logger.info("\u6B63\u5728\u4FEE\u590D\u7B14\u8BB0\u65F6\u95F4\u6233...");
4169
+ }
4170
+ const rootConfigFixed = this.fixRootConfigTimestamps(forceUpdate);
4171
+ if (rootConfigFixed) {
4172
+ logger.success("\u2705 \u6839\u914D\u7F6E\u6587\u4EF6\u65F6\u95F4\u6233\u5DF2\u4FEE\u590D");
4173
+ }
4174
+ if (!existsSync9(NOTES_DIR_PATH)) {
4175
+ logger.error("notes \u76EE\u5F55\u4E0D\u5B58\u5728");
4176
+ return { fixed: 0, skipped: 0, total: 0, rootConfigFixed };
4177
+ }
4178
+ const noteDirs = readdirSync4(NOTES_DIR_PATH).filter((name) => {
4179
+ const fullPath = join10(NOTES_DIR_PATH, name);
4180
+ return statSync2(fullPath).isDirectory() && /^\d{4}\./.test(name);
4181
+ }).sort();
4182
+ let fixedCount = 0;
4183
+ let skippedCount = 0;
4184
+ for (const noteDir of noteDirs) {
4185
+ const fixed = this.fixNoteTimestamps(noteDir, forceUpdate);
4186
+ if (fixed) {
4187
+ fixedCount++;
4188
+ } else {
4189
+ skippedCount++;
4190
+ }
4191
+ }
4192
+ if (fixedCount > 0) {
4193
+ logger.success(`\u65F6\u95F4\u6233\u4FEE\u590D\u5B8C\u6210: ${fixedCount} \u4E2A\u7B14\u8BB0\u5DF2\u66F4\u65B0`);
4194
+ } else {
4195
+ logger.info("\u6240\u6709\u7B14\u8BB0\u65F6\u95F4\u6233\u5747\u5DF2\u6B63\u786E");
4196
+ }
4197
+ return {
4198
+ fixed: fixedCount,
4199
+ skipped: skippedCount,
4200
+ total: noteDirs.length,
4201
+ rootConfigFixed
4202
+ };
4203
+ }
4204
+ /**
4205
+ * 更新指定笔记的时间戳为当前时间
4206
+ * @param noteDirNames - 笔记目录名数组
4207
+ * @returns 更新的笔记数量
4208
+ */
4209
+ async updateNotesTimestamp(noteDirNames) {
4210
+ if (noteDirNames.length === 0) {
4211
+ return 0;
4212
+ }
4213
+ const now = Date.now();
4214
+ let updatedCount = 0;
4215
+ for (const noteDir of noteDirNames) {
4216
+ const configPath = join10(NOTES_DIR_PATH, noteDir, ".tnotes.json");
4217
+ if (!existsSync9(configPath)) {
4218
+ continue;
4219
+ }
4220
+ try {
4221
+ const configContent = readFileSync6(configPath, "utf-8");
4222
+ const config2 = JSON.parse(configContent);
4223
+ config2.updated_at = now;
4224
+ this.noteManager.writeNoteConfig(configPath, config2);
4225
+ updatedCount++;
4226
+ } catch (error) {
4227
+ logger.error(`\u66F4\u65B0\u65F6\u95F4\u6233\u5931\u8D25: ${noteDir}`, error);
4228
+ }
4229
+ }
4230
+ return updatedCount;
4231
+ }
4232
+ /**
4233
+ * 获取本次变更中包含 README.md 的笔记列表
4234
+ * @param changedFiles - git status 返回的变更文件列表
4235
+ * @returns 变更的笔记目录名数组
4236
+ */
4237
+ getChangedNotes(changedFiles) {
4238
+ const changedNotes = /* @__PURE__ */ new Set();
4239
+ for (let file of changedFiles) {
4240
+ file = file.replace(/^"(.*)"$/, "$1").replace(/"$/, "");
4241
+ const match = file.match(/^notes\/([^/]+)\/README\.md$/);
4242
+ if (match) {
4243
+ changedNotes.add(match[1]);
4244
+ }
4245
+ }
4246
+ return Array.from(changedNotes);
4247
+ }
4248
+ };
4249
+
4250
+ // services/vitepress/service.ts
4251
+ import { spawn as spawn2 } from "child_process";
4252
+ var VitepressService = class _VitepressService {
4253
+ /** VitePress 开发服务器默认端口 */
4254
+ static DEFAULT_DEV_PORT = 5173;
4255
+ /** VitePress 预览服务器默认端口 */
4256
+ static DEFAULT_PREVIEW_PORT = 4173;
4257
+ /** 开发服务器进程 ID 后缀 */
4258
+ static PROCESS_ID_DEV_SUFFIX = "vitepress-dev";
4259
+ /** 预览服务器进程 ID 后缀 */
4260
+ static PROCESS_ID_PREVIEW_SUFFIX = "vitepress-preview";
4261
+ /** 服务启动超时时间(毫秒) */
4262
+ static SERVER_STARTUP_TIMEOUT = 6e4;
4263
+ /** 端口释放等待超时时间(毫秒) */
4264
+ static PORT_RELEASE_TIMEOUT = 3e3;
4265
+ /** 进程清理等待时间(毫秒) */
4266
+ static PROCESS_CLEANUP_DELAY = 3e3;
4267
+ /** 显示启动服务状态行间隔(毫秒) */
4268
+ static SERVER_STATUS_LINE_INTERVAL = 1e3;
4269
+ /** 默认包管理器 */
4270
+ static DEFAULT_PACKAGE_MANAGER = "pnpm";
4271
+ processManager;
4272
+ configManager;
4273
+ constructor() {
4274
+ this.processManager = new ProcessManager();
4275
+ this.configManager = ConfigManager.getInstance();
4276
+ }
4277
+ /**
4278
+ * 启动 VitePress 开发服务器
4279
+ * @returns 启动结果(服务就绪后返回),失败时返回 undefined
4280
+ */
4281
+ async startServer() {
4282
+ const port2 = this.configManager.get("port") || _VitepressService.DEFAULT_DEV_PORT;
4283
+ const repoName2 = this.configManager.get("repoName");
4284
+ const processId = `${repoName2}-${_VitepressService.PROCESS_ID_DEV_SUFFIX}`;
4285
+ if (this.processManager.has(processId) && this.processManager.isRunning(processId)) {
4286
+ this.processManager.kill(processId);
4287
+ await new Promise(
4288
+ (resolve3) => setTimeout(resolve3, _VitepressService.PROCESS_CLEANUP_DELAY)
4289
+ );
4290
+ }
4291
+ if (isPortInUse(port2)) {
4292
+ logger.warn(`\u7AEF\u53E3 ${port2} \u88AB\u5360\u7528\uFF0C\u6B63\u5728\u6E05\u7406...`);
4293
+ killPortProcess(port2);
4294
+ const available = await waitForPort(
4295
+ port2,
4296
+ _VitepressService.PORT_RELEASE_TIMEOUT
4297
+ );
4298
+ if (available) {
4299
+ logger.info(`\u7AEF\u53E3 ${port2} \u5DF2\u91CA\u653E\uFF0C\u7EE7\u7EED\u542F\u52A8\u670D\u52A1`);
4300
+ } else {
4301
+ logger.warn(
4302
+ `\u7AEF\u53E3 ${port2} \u672A\u786E\u8BA4\u91CA\u653E\uFF0C\u4ECD\u5C06\u5C1D\u8BD5\u542F\u52A8\uFF1B\u5982\u542F\u52A8\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u6E05\u7406\u8BE5\u7AEF\u53E3`
4303
+ );
4304
+ }
4305
+ }
4306
+ const pm = this.configManager.get("packageManager") || _VitepressService.DEFAULT_PACKAGE_MANAGER;
4307
+ const args = ["vitepress", "dev", "--port", port2.toString()];
4308
+ const processInfo = this.processManager.spawn(processId, pm, args, {
4309
+ cwd: ROOT_DIR_PATH,
4310
+ stdio: ["inherit", "pipe", "pipe"]
4311
+ // stdin 继承,stdout/stderr 管道捕获
4312
+ });
4313
+ const serverInfo = await this.waitForServerReady(processInfo.process);
4314
+ if (!processInfo.pid) return void 0;
4315
+ return {
4316
+ pid: processInfo.pid,
4317
+ version: serverInfo.version,
4318
+ elapsed: serverInfo.elapsed
4319
+ };
4320
+ }
4321
+ /**
4322
+ * 等待服务就绪,显示启动状态
4323
+ * @param childProcess - 子进程
4324
+ */
4325
+ waitForServerReady(childProcess) {
4326
+ return new Promise((resolve3) => {
4327
+ const startTime = Date.now();
4328
+ let serverReady = false;
4329
+ let version = "";
4330
+ const statusTimer = setInterval(() => {
4331
+ if (serverReady) {
4332
+ clearInterval(statusTimer);
4333
+ return;
4334
+ }
4335
+ const elapsed = Date.now() - startTime;
4336
+ const seconds = (elapsed / 1e3).toFixed(0);
4337
+ process.stderr.clearLine?.(0);
4338
+ process.stderr.cursorTo?.(0);
4339
+ process.stderr.write(`\u23F3 \u542F\u52A8\u4E2D: \u5DF2\u7528 ${seconds}s...`);
4340
+ }, _VitepressService.SERVER_STATUS_LINE_INTERVAL);
4341
+ const handleOutput = (data) => {
4342
+ const text = data.toString();
4343
+ const versionMatch = text.match(/vitepress v([\d.]+)/);
4344
+ if (versionMatch) {
4345
+ version = versionMatch[1];
4346
+ }
4347
+ if (!serverReady && (text.includes("Local:") || text.includes("http://localhost") || text.includes("\u279C") && text.includes("Local"))) {
4348
+ serverReady = true;
4349
+ clearInterval(statusTimer);
4350
+ process.stderr.clearLine?.(0);
4351
+ process.stderr.cursorTo?.(0);
4352
+ const elapsed = Date.now() - startTime;
4353
+ setTimeout(() => resolve3({ version, elapsed }), 200);
4354
+ return;
4355
+ }
4356
+ if (!serverReady) {
4357
+ if (text.includes("error") || text.includes("Error") || text.includes("Port") && text.includes("is in use")) {
4358
+ process.stderr.clearLine?.(0);
4359
+ process.stderr.cursorTo?.(0);
4360
+ process.stdout.write(data);
4361
+ }
4362
+ }
4363
+ };
4364
+ if (childProcess.stdout) {
4365
+ childProcess.stdout.setEncoding("utf8");
4366
+ childProcess.stdout.on("data", handleOutput);
4367
+ }
4368
+ if (childProcess.stderr) {
4369
+ childProcess.stderr.setEncoding("utf8");
4370
+ childProcess.stderr.on("data", handleOutput);
4371
+ }
4372
+ setTimeout(() => {
4373
+ if (!serverReady) {
4374
+ serverReady = true;
4375
+ clearInterval(statusTimer);
4376
+ process.stderr.clearLine?.(0);
4377
+ process.stderr.cursorTo?.(0);
4378
+ logger.warn("\u542F\u52A8\u8D85\u65F6\uFF0C\u8BF7\u68C0\u67E5 VitePress \u8F93\u51FA");
4379
+ resolve3({ version, elapsed: _VitepressService.SERVER_STARTUP_TIMEOUT });
4380
+ }
4381
+ }, _VitepressService.SERVER_STARTUP_TIMEOUT);
4382
+ });
4383
+ }
4384
+ /**
4385
+ * 构建生产版本
4386
+ */
4387
+ build() {
4388
+ return new Promise((resolve3, reject) => {
4389
+ const pm = this.configManager.get("packageManager") || _VitepressService.DEFAULT_PACKAGE_MANAGER;
4390
+ const child = spawn2(pm, ["vitepress", "build"], {
4391
+ cwd: ROOT_DIR_PATH,
4392
+ shell: true,
4393
+ stdio: ["inherit", "pipe", "pipe"]
4394
+ });
4395
+ const filterOutput = (data) => {
4396
+ const str = data.toString();
4397
+ if (str.includes("\u{1F528}") || str.includes("\u2705 \u6784\u5EFA\u6210\u529F") || str.includes("\u274C \u6784\u5EFA\u5931\u8D25") || str.includes("\u{1F4C1}") || str.includes("\u{1F4CA}") || str.includes("\u{1F4E6}") || str.includes("\u23F1\uFE0F") || str.includes("Building [") || str.includes("error") || str.includes("Error")) {
4398
+ process.stdout.write(data);
4399
+ return;
4400
+ }
4401
+ if (/^[\s⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✓\r\n]*$/.test(str) || str.includes("building client + server") || str.includes("rendering pages") || str.includes("generating sitemap") || str.includes("build complete in") || str.includes("vitepress v")) {
4402
+ return;
4403
+ }
4404
+ };
4405
+ child.stdout?.on("data", filterOutput);
4406
+ child.stderr?.on("data", filterOutput);
4407
+ child.on("error", (err) => {
4408
+ reject(err);
4409
+ });
4410
+ child.on("close", (code) => {
4411
+ if (code === 0) {
4412
+ resolve3();
4413
+ } else {
4414
+ reject(new Error(`Command failed with code ${code}`));
4415
+ }
4416
+ });
4417
+ });
4418
+ }
4419
+ /**
4420
+ * 预览构建后的站点
4421
+ */
4422
+ async preview() {
4423
+ const repoName2 = this.configManager.get("repoName");
4424
+ const processId = `${repoName2}-${_VitepressService.PROCESS_ID_PREVIEW_SUFFIX}`;
4425
+ const pm = this.configManager.get("packageManager") || _VitepressService.DEFAULT_PACKAGE_MANAGER;
4426
+ const args = ["vitepress", "preview"];
4427
+ const previewPort = _VitepressService.DEFAULT_PREVIEW_PORT;
4428
+ if (isPortInUse(previewPort)) {
4429
+ logger.warn(`\u7AEF\u53E3 ${previewPort} \u5DF2\u88AB\u5360\u7528\uFF0C\u6B63\u5728\u5C1D\u8BD5\u6E05\u7406...`);
4430
+ const killed = killPortProcess(previewPort);
4431
+ if (killed) {
4432
+ const available = await waitForPort(
4433
+ previewPort,
4434
+ _VitepressService.PORT_RELEASE_TIMEOUT
4435
+ );
4436
+ if (!available) {
4437
+ logger.error(`\u7AEF\u53E3 ${previewPort} \u91CA\u653E\u8D85\u65F6\uFF0C\u8BF7\u624B\u52A8\u6E05\u7406`);
4438
+ return void 0;
4439
+ }
4440
+ logger.info(`\u7AEF\u53E3 ${previewPort} \u5DF2\u91CA\u653E`);
4441
+ } else {
4442
+ logger.error(
4443
+ `\u65E0\u6CD5\u6E05\u7406\u7AEF\u53E3 ${previewPort}\uFF0C\u8BF7\u624B\u52A8\u6267\u884C: taskkill /F /PID <PID>`
4444
+ );
4445
+ return void 0;
4446
+ }
4447
+ }
4448
+ const processInfo = this.processManager.spawn(processId, pm, args, {
4449
+ cwd: ROOT_DIR_PATH,
4450
+ stdio: "inherit"
4451
+ });
4452
+ return processInfo.pid;
4453
+ }
4454
+ };
4455
+
4456
+ // commands/note/UpdateNoteConfigCommand.ts
4457
+ var UpdateNoteConfigCommand = class extends BaseCommand {
4458
+ noteService;
4459
+ constructor() {
4460
+ super("update-note-config");
4461
+ this.noteService = NoteService.getInstance();
4462
+ }
4463
+ async run() {
4464
+ const noteIndex = process.env.NOTE_ID;
4465
+ const done = process.env.NOTE_DONE === "true";
4466
+ const enableDiscussions = process.env.NOTE_DISCUSSIONS === "true";
4467
+ const description = process.env.NOTE_DESCRIPTION || "";
4468
+ if (!noteIndex) {
4469
+ throw new Error("\u7F3A\u5C11 NOTE_ID \u53C2\u6570");
4470
+ }
4471
+ try {
4472
+ await this.updateConfig({
4473
+ noteIndex,
4474
+ config: {
4475
+ done,
4476
+ enableDiscussions,
4477
+ description
4478
+ }
4479
+ });
4480
+ this.logger.success(`\u7B14\u8BB0 ${noteIndex} \u914D\u7F6E\u5DF2\u66F4\u65B0`);
4481
+ } catch (error) {
4482
+ this.logger.error("\u66F4\u65B0\u914D\u7F6E\u5931\u8D25", error);
4483
+ throw error;
4484
+ }
4485
+ }
4486
+ /**
4487
+ * 更新笔记配置(可被外部调用)
4488
+ */
4489
+ async updateConfig(params) {
4490
+ const { noteIndex, config: config2 } = params;
4491
+ const note = this.noteService.getNoteByIndex(noteIndex);
4492
+ if (!note) {
4493
+ throw new Error(`\u7B14\u8BB0\u672A\u627E\u5230: ${noteIndex}`);
4494
+ }
4495
+ await this.noteService.updateNoteConfig(noteIndex, config2);
4496
+ this.logger.info(`\u2705 \u7B14\u8BB0 ${noteIndex} \u914D\u7F6E\u5DF2\u66F4\u65B0:`);
4497
+ if (config2.done !== void 0)
4498
+ this.logger.info(` - \u5B8C\u6210\u72B6\u6001: ${config2.done}`);
4499
+ if (config2.enableDiscussions !== void 0)
4500
+ this.logger.info(` - \u8BC4\u8BBA\u72B6\u6001: ${config2.enableDiscussions}`);
4501
+ if (config2.description !== void 0)
4502
+ this.logger.info(` - \u7B14\u8BB0\u7B80\u4ECB: ${config2.description || "(\u7A7A)"}`);
4503
+ }
4504
+ };
4505
+
4506
+ // commands/note/RenameNoteCommand.ts
4507
+ import { existsSync as existsSync10, renameSync, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
4508
+ import { join as join11 } from "path";
4509
+ var RenameNoteCommand = class extends BaseCommand {
4510
+ noteService;
4511
+ readmeService;
4512
+ constructor() {
4513
+ super("rename-note");
4514
+ this.noteService = NoteService.getInstance();
4515
+ this.readmeService = ReadmeService.getInstance();
4516
+ }
4517
+ async run() {
4518
+ const noteIndex = process.env.NOTE_ID;
4519
+ const newTitle = process.env.NOTE_TITLE;
4520
+ if (!noteIndex || !newTitle) {
4521
+ throw new Error("\u7F3A\u5C11 NOTE_ID \u6216 NOTE_TITLE \u53C2\u6570");
4522
+ }
4523
+ try {
4524
+ await this.renameNote({ noteIndex, newTitle });
4525
+ this.logger.success(`\u7B14\u8BB0 ${noteIndex} \u5DF2\u91CD\u547D\u540D\u4E3A: ${newTitle}`);
4526
+ } catch (error) {
4527
+ this.logger.error("\u91CD\u547D\u540D\u5931\u8D25", error);
4528
+ throw error;
4529
+ }
4530
+ }
4531
+ /**
4532
+ * 重命名笔记(可被外部调用)
4533
+ */
4534
+ async renameNote(params) {
4535
+ const { noteIndex, newTitle } = params;
4536
+ const note = this.noteService.getNoteByIndex(noteIndex);
4537
+ if (!note) {
4538
+ throw new Error(`\u7B14\u8BB0\u672A\u627E\u5230: ${noteIndex}`);
4539
+ }
4540
+ const validation = validateNoteTitle(newTitle);
4541
+ if (!validation.valid) {
4542
+ throw new Error(validation.error || "\u6807\u9898\u683C\u5F0F\u65E0\u6548");
4543
+ }
4544
+ const newDirName = `${noteIndex}. ${newTitle.trim()}`;
4545
+ const newPath = join11(NOTES_PATH, newDirName);
4546
+ if (existsSync10(newPath)) {
4547
+ throw new Error(`\u76EE\u6807\u6587\u4EF6\u5939\u5DF2\u5B58\u5728: ${newDirName}`);
4548
+ }
4549
+ try {
4550
+ renameSync(note.path, newPath);
4551
+ this.logger.info(`\u2705 \u6587\u4EF6\u5939\u5DF2\u91CD\u547D\u540D:`);
4552
+ this.logger.info(` \u539F\u540D\u79F0: ${note.dirName}`);
4553
+ this.logger.info(` \u65B0\u540D\u79F0: ${newDirName}`);
4554
+ } catch (error) {
4555
+ throw new Error(
4556
+ `\u91CD\u547D\u540D\u6587\u4EF6\u5939\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`
4557
+ );
4558
+ }
4559
+ try {
4560
+ this.logger.info("\u6B63\u5728\u66F4\u65B0\u7B14\u8BB0\u5185\u90E8\u6807\u9898...");
4561
+ const readmePath = join11(newPath, "README.md");
4562
+ if (existsSync10(readmePath)) {
4563
+ const content = readFileSync7(readmePath, "utf-8");
4564
+ const lines = content.split("\n");
4565
+ let h1Index = -1;
4566
+ for (let i = 0; i < lines.length; i++) {
4567
+ if (lines[i].trim().startsWith("# ")) {
4568
+ h1Index = i;
4569
+ break;
4570
+ }
4571
+ }
4572
+ if (h1Index !== -1) {
4573
+ const newH1 = generateNoteTitle(
4574
+ noteIndex,
4575
+ newTitle.trim(),
4576
+ REPO_NOTES_URL
4577
+ );
4578
+ lines[h1Index] = newH1;
4579
+ writeFileSync6(readmePath, lines.join("\n"), "utf-8");
4580
+ this.logger.success("\u2705 \u7B14\u8BB0\u6807\u9898\u5DF2\u66F4\u65B0");
4581
+ } else {
4582
+ this.logger.warn(
4583
+ `\u26A0\uFE0F \u7B14\u8BB0\u6807\u9898\u683C\u5F0F\u4E0D\u7B26\u5408\u89C4\u8303\uFF0C\u672A\u627E\u5230\u4E00\u7EA7\u6807\u9898\uFF0C\u8BF7\u624B\u52A8\u68C0\u67E5\u4FEE\u6B63: ${readmePath}`
4584
+ );
4585
+ }
4586
+ }
4587
+ } catch (error) {
4588
+ this.logger.warn("\u26A0\uFE0F \u66F4\u65B0\u7B14\u8BB0\u6807\u9898\u65F6\u51FA\u9519:", error);
4589
+ }
4590
+ try {
4591
+ this.logger.info("\u6B63\u5728\u66F4\u65B0\u5168\u5C40 README.md \u548C sidebar.json...");
4592
+ await this.readmeService.updateAllReadmes();
4593
+ this.logger.success("\u2705 \u5168\u5C40\u6587\u4EF6\u5DF2\u66F4\u65B0");
4594
+ } catch (error) {
4595
+ this.logger.warn("\u26A0\uFE0F \u6587\u4EF6\u5939\u91CD\u547D\u540D\u6210\u529F,\u4F46\u66F4\u65B0\u5168\u5C40\u6587\u4EF6\u65F6\u51FA\u9519:", error);
4596
+ }
4597
+ }
4598
+ };
4599
+
4600
+ export {
4601
+ COMMAND_NAMES,
4602
+ COMMAND_DESCRIPTIONS,
4603
+ COMMAND_OPTIONS,
4604
+ handleError,
4605
+ generateAnchor,
4606
+ getTargetDirs,
4607
+ logger,
4608
+ createLogger,
4609
+ parseArgs,
4610
+ parseReadmeCompletedNotes,
4611
+ TNOTES_BASE_DIR,
4612
+ EN_WORDS_DIR,
4613
+ ROOT_DIR_PATH,
4614
+ ROOT_CONFIG_PATH,
4615
+ NoteManager,
4616
+ runCommand,
4617
+ pushAllRepos,
4618
+ pullAllRepos,
4619
+ syncAllRepos,
4620
+ BaseCommand,
4621
+ NoteIndexCache,
4622
+ ReadmeService,
4623
+ NoteService,
4624
+ FileWatcherService,
4625
+ GitService,
4626
+ SyncCoreService,
4627
+ TimestampService,
4628
+ VitepressService,
4629
+ UpdateNoteConfigCommand,
4630
+ RenameNoteCommand
4631
+ };