@tnotesjs/core 0.1.2 → 0.1.3

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.

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