tcdona_unilib 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Eff 框架:基于 Result 的异步任务管理
3
+ * - root/create:创建异步任务
4
+ * - all/any/race/allSettled:并行或竞速执行多个任务
5
+ * - Signal 传播:支持任务取消和自动清理
6
+ */
7
+ import type { Result } from 'neverthrow';
8
+ import { err, ok, ResultAsync } from 'neverthrow';
9
+
10
+ export interface EffContext { signal: AbortSignal }
11
+
12
+ type EffExecutor<T, E> = (ctx: EffContext) => PromiseLike<Result<T, E>>;
13
+ type NonEmptyArray<T> = readonly [T, ...T[]];
14
+
15
+ const ABORT_REASON = { SETTLED: 'settled', COMPLETED: 'completed' } as const;
16
+ const noop = () => {};
17
+
18
+ /**
19
+ * 将父 signal abort 传播到子 controller,返回解绑函数
20
+ */
21
+ const linkSignal = (
22
+ parentSignal: AbortSignal,
23
+ controller: AbortController,
24
+ ): (() => void) => {
25
+ if (parentSignal.aborted) {
26
+ controller.abort(parentSignal.reason);
27
+ return noop;
28
+ }
29
+ const handler = () => controller.abort(parentSignal.reason);
30
+ parentSignal.addEventListener('abort', handler, { once: true });
31
+ return () => parentSignal.removeEventListener('abort', handler);
32
+ };
33
+
34
+ /**
35
+ * 并行运行多个任务,通过回调函数允许调用方控制何时 settle
36
+ */
37
+ const runAll = <T, E>(
38
+ executors: NonEmptyArray<EffExecutor<T, E>>,
39
+ parentSignal: AbortSignal,
40
+ onResult: (
41
+ result: Result<T, E>,
42
+ index: number,
43
+ settle: (r: Result<unknown, unknown>) => void,
44
+ context: {
45
+ resultCount: number;
46
+ okValues: T[];
47
+ errValues: E[];
48
+ allResults: Result<T, E>[];
49
+ },
50
+ ) => void,
51
+ ): ResultAsync<unknown, unknown> =>
52
+ Eff.create(({ signal }) => {
53
+ const controller = new AbortController();
54
+ const childSignal = controller.signal;
55
+ const unlink = linkSignal(signal, controller);
56
+
57
+ return new Promise((resolve) => {
58
+ let done = false;
59
+ const context = {
60
+ resultCount: 0,
61
+ okValues: [] as T[],
62
+ errValues: [] as E[],
63
+ allResults: [] as Result<T, E>[],
64
+ };
65
+
66
+ const settle = (r: Result<unknown, unknown>) => {
67
+ if (done) return;
68
+ done = true;
69
+ unlink();
70
+ controller.abort(ABORT_REASON.SETTLED);
71
+ resolve(r);
72
+ };
73
+
74
+ if (signal.aborted) {
75
+ settle(ok([]));
76
+ return;
77
+ }
78
+
79
+ let remaining = executors.length;
80
+
81
+ executors.forEach((executor, i) => {
82
+ Eff.create(executor, childSignal).then((r) => {
83
+ if (done) return;
84
+ remaining--;
85
+ onResult(r, i, settle, context);
86
+ if (remaining === 0) settle(ok(context.allResults));
87
+ });
88
+ });
89
+ });
90
+ }, parentSignal);
91
+
92
+ export class Eff {
93
+ /**
94
+ * 创建根级任务(无父 signal)
95
+ */
96
+ static root<T, E>(executor: EffExecutor<T, E>): ResultAsync<T, E> {
97
+ const controller = new AbortController();
98
+ const promise: Promise<Result<T, E>> = Promise.resolve()
99
+ .then(() => executor({ signal: controller.signal }))
100
+ .catch((e) => err(e as E))
101
+ .finally(() => controller.abort(ABORT_REASON.COMPLETED));
102
+ return new ResultAsync(promise);
103
+ }
104
+
105
+ /**
106
+ * 创建子任务(继承父 signal,支持取消传播)
107
+ */
108
+ static create<T, E>(
109
+ executor: EffExecutor<T, E>,
110
+ parentSignal: AbortSignal,
111
+ ): ResultAsync<T, E> {
112
+ const controller = new AbortController();
113
+ const unlink = linkSignal(parentSignal, controller);
114
+
115
+ const promise: Promise<Result<T, E>> = Promise.resolve()
116
+ .then(() => executor({ signal: controller.signal }))
117
+ .catch((e) => err(e as E))
118
+ .finally(() => {
119
+ unlink();
120
+ controller.abort(ABORT_REASON.COMPLETED);
121
+ });
122
+
123
+ return new ResultAsync(promise);
124
+ }
125
+
126
+ /**
127
+ * 全部成功才成功,任一失败立即失败并 abort 其他
128
+ */
129
+ static all<T, E>(
130
+ executors: NonEmptyArray<EffExecutor<T, E>>,
131
+ parentSignal: AbortSignal,
132
+ ): ResultAsync<T[], E> {
133
+ return runAll<T, E>(
134
+ executors,
135
+ parentSignal,
136
+ (result, index, settle, context) => {
137
+ result.match(
138
+ (v) => {
139
+ context.okValues[index] = v;
140
+ context.resultCount++;
141
+ if (context.resultCount === executors.length) {
142
+ settle(ok(context.okValues));
143
+ }
144
+ },
145
+ (e) => settle(err(e)),
146
+ );
147
+ },
148
+ ) as ResultAsync<T[], E>;
149
+ }
150
+
151
+ /**
152
+ * 任一成功立即成功并 abort 其他,全部失败才失败
153
+ */
154
+ static any<T, E>(
155
+ executors: NonEmptyArray<EffExecutor<T, E>>,
156
+ parentSignal: AbortSignal,
157
+ ): ResultAsync<T, E[]> {
158
+ return runAll<T, E>(
159
+ executors,
160
+ parentSignal,
161
+ (result, index, settle, context) => {
162
+ result.match(
163
+ (v) => settle(ok(v)),
164
+ (e) => {
165
+ context.errValues[index] = e;
166
+ context.resultCount++;
167
+ if (context.resultCount === executors.length) {
168
+ settle(err(context.errValues));
169
+ }
170
+ },
171
+ );
172
+ },
173
+ ) as ResultAsync<T, E[]>;
174
+ }
175
+
176
+ /**
177
+ * 任一完成立即返回其结果并 abort 其他
178
+ */
179
+ static race<T, E>(
180
+ executors: NonEmptyArray<EffExecutor<T, E>>,
181
+ parentSignal: AbortSignal,
182
+ ): ResultAsync<T, E> {
183
+ return runAll<T, E>(executors, parentSignal, (result, _index, settle) => {
184
+ settle(result);
185
+ }) as ResultAsync<T, E>;
186
+ }
187
+
188
+ /**
189
+ * 等待全部完成,收集所有结果(不因某个失败而 abort)
190
+ */
191
+ static allSettled<T, E>(
192
+ executors: NonEmptyArray<EffExecutor<T, E>>,
193
+ parentSignal: AbortSignal,
194
+ ): ResultAsync<Result<T, E>[], never> {
195
+ return runAll<T, E>(
196
+ executors,
197
+ parentSignal,
198
+ (result, index, _settle, context) => {
199
+ context.allResults[index] = result;
200
+ },
201
+ ) as ResultAsync<Result<T, E>[], never>;
202
+ }
203
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * 日志和错误处理 API:基于文件路径和事件 ID 生成统一的日志前缀
3
+ */
4
+
5
+ // ==================== 日志配置 ====================
6
+
7
+ const cons = console;
8
+
9
+ export enum LogLevel {
10
+ DEBUG = 0,
11
+ INFO = 1,
12
+ WARN = 2,
13
+ ERROR = 3,
14
+ FATAL = 4,
15
+ }
16
+
17
+ const logLevelUsed = LogLevel.INFO;
18
+
19
+ const logFnMap: Record<LogLevel, (...args: unknown[]) => void> = {
20
+ [LogLevel.DEBUG]: cons.debug,
21
+ [LogLevel.INFO]: cons.log,
22
+ [LogLevel.WARN]: cons.warn,
23
+ [LogLevel.ERROR]: cons.error,
24
+ [LogLevel.FATAL]: cons.error,
25
+ };
26
+
27
+ // ==================== 日志链类型 ====================
28
+
29
+ export interface TcLog {
30
+ debug: (...args: unknown[]) => void;
31
+ info: (...args: unknown[]) => void;
32
+ log: (...args: unknown[]) => void;
33
+ warn: (...args: unknown[]) => void;
34
+ error: (...args: unknown[]) => void;
35
+ fatal: (...args: unknown[]) => void;
36
+ }
37
+
38
+ // ==================== 新的 fileId 配置类型 ====================
39
+
40
+ /**
41
+ * fileId 的配置对象:包含事件 ID、源文件、标签
42
+ */
43
+ export interface FileIdConf {
44
+ id: string; // 事件标识 key
45
+ srcFile: string; // 源文件路径
46
+ tag: string[]; // 标签数组(必须)
47
+ }
48
+
49
+ /**
50
+ * fileId:包含配置和日志方法(info, warn, error, debug)
51
+ */
52
+ export interface FileId {
53
+ readonly conf: FileIdConf;
54
+
55
+ // 日志方法
56
+ info: (...args: unknown[]) => void;
57
+ warn: (...args: unknown[]) => void;
58
+ error: (...args: unknown[]) => void;
59
+ debug: (...args: unknown[]) => void;
60
+ }
61
+
62
+ /**
63
+ * 生成格式为 YYMMDD-HHmmss 的时间戳
64
+ */
65
+ const getTimestamp = (): string => {
66
+ const now = new Date();
67
+ const yy = String(now.getFullYear()).slice(-2).padStart(2, '0');
68
+ const mm = String(now.getMonth() + 1).padStart(2, '0');
69
+ const dd = String(now.getDate()).padStart(2, '0');
70
+ const hh = String(now.getHours()).padStart(2, '0');
71
+ const min = String(now.getMinutes()).padStart(2, '0');
72
+ const ss = String(now.getSeconds()).padStart(2, '0');
73
+ return `${yy}${mm}${dd}-${hh}${min}${ss}`;
74
+ };
75
+
76
+ export class TcErr extends Error {
77
+ public fildId: FileId;
78
+ public ctx: Record<string, unknown>;
79
+ public cause?: Error; // 显式声明 cause 属性
80
+
81
+ constructor(
82
+ ctx: { fildId: FileId; cause?: unknown } & Record<string, unknown>,
83
+ ) {
84
+ const { fildId, cause, ...rest } = ctx;
85
+ super(fildId.conf?.id ?? 'TcErrId');
86
+ Object.setPrototypeOf(this, new.target.prototype);
87
+ this.name = 'TcErr';
88
+ this.fildId = fildId;
89
+ if (cause) {
90
+ this.cause = cause as Error;
91
+ }
92
+ this.ctx = rest;
93
+ if (Error.captureStackTrace) {
94
+ Error.captureStackTrace(this, TcErr);
95
+ }
96
+
97
+ cons.error(getTimestamp(), fildId, cause, rest);
98
+ }
99
+ }
100
+
101
+ // ==================== 内部日志函数工厂 ====================
102
+
103
+ const createLogFn = (
104
+ level: LogLevel,
105
+ prefix: string,
106
+ ): ((...args: unknown[]) => void) => {
107
+ if (level < logLevelUsed) return () => { };
108
+
109
+ const logFn = logFnMap[level] ?? (() => { });
110
+ return logFn.bind(null, `${getTimestamp()} ${prefix}`);
111
+ };
112
+
113
+ // ==================== 新的 fileInit API ====================
114
+
115
+ /**
116
+ * 工厂函数类型:接收事件 ID 和可选标签,返回 FileId
117
+ */
118
+ export type fileIdFactory = (id: string, ...tag: string[]) => FileId;
119
+
120
+ /**
121
+ * 为模块创建 fileId 工厂函数
122
+ * @param modulePath 模块路径(相对路径字符串)
123
+ */
124
+ export function fileInit(modulePath: string): fileIdFactory {
125
+ return (id: string, ...tag: string[]): FileId => {
126
+ const prefix = `[${modulePath}:${id}]${tag.length > 0 ? `[${tag.join(', ')}]` : ''}`;
127
+
128
+ const conf: FileIdConf = {
129
+ id,
130
+ srcFile: modulePath,
131
+ tag,
132
+ };
133
+
134
+ return {
135
+ conf,
136
+ info: createLogFn(LogLevel.INFO, prefix),
137
+ warn: createLogFn(LogLevel.WARN, prefix),
138
+ error: createLogFn(LogLevel.ERROR, prefix),
139
+ debug: createLogFn(LogLevel.DEBUG, prefix),
140
+ };
141
+ };
142
+ }
@@ -0,0 +1,18 @@
1
+ import { fs, YAML } from 'zx';
2
+
3
+ /**
4
+ * YAML 文件读写:无 data 参数时读取,有 data 参数时写入
5
+ */
6
+ export function yml(path: string, data?: unknown): unknown {
7
+ if (typeof data !== 'undefined') {
8
+ fs.writeFileSync(
9
+ path,
10
+ // @ts-expect-error YAML.stringify 类型定义有误
11
+ YAML.stringify(data, { singleQuote: true, sortKeys: true }),
12
+ 'utf-8',
13
+ );
14
+ } else {
15
+ const ymlContent = fs.readFileSync(path, 'utf-8');
16
+ return YAML.parse(ymlContent);
17
+ }
18
+ }
@@ -0,0 +1,320 @@
1
+ /**
2
+ * fileId 调用规范检查:
3
+ * - 检查 fileInit 导入是否被重命名
4
+ * - 检查 fileInit 返回值是否命名为 fileId
5
+ * - 检查 fileId 调用中的 key 是否重复
6
+ */
7
+
8
+ import { err, ok } from 'neverthrow';
9
+ import type { EffContext } from './eff.js';
10
+ import { Eff } from './eff.js';
11
+ import type { Result, ResultAsync } from 'neverthrow';
12
+ import { fileInit } from './enum.api.js';
13
+ import { parseFileAsync, scanTypeScriptFilesAsync } from './ast.scan.js';
14
+ import { extractKeyFromCall, findAllByKind, findFunctionCalls } from './ast.js';
15
+ import type { AstNode } from './ast.js';
16
+
17
+ type NonEmptyArray<T> = readonly [T, ...T[]];
18
+
19
+ // ==================== 日志配置 ====================
20
+
21
+ const fileId = fileInit('staticMeta/iduniq.ts');
22
+
23
+ // ==================== 类型定义 ====================
24
+
25
+ interface KeyUsage {
26
+ readonly key: string;
27
+ readonly tags?: string[];
28
+ }
29
+
30
+ interface CheckIssue {
31
+ readonly filePath: string;
32
+ readonly type: 'import-violation' | 'variable-name-violation' | 'duplicate';
33
+ readonly details: string;
34
+ readonly keys?: readonly {
35
+ key: string;
36
+ count: number;
37
+ }[];
38
+ }
39
+
40
+ // ==================== 导入规范检查 ====================
41
+
42
+ /**
43
+ * 检查 fileInit 导入是否被 as 重命名(违反规范)
44
+ */
45
+ function checkImportConvention(root: AstNode): Result<boolean, CheckIssue> {
46
+ const importStatements = findAllByKind(root, 'import_statement');
47
+
48
+ for (const stmt of importStatements) {
49
+ const specifiers = findAllByKind(stmt, 'import_specifier');
50
+ for (const spec of specifiers) {
51
+ const children = spec.children();
52
+ // import_specifier 的结构: identifier | (identifier 'as' identifier)
53
+ if (children.length >= 1) {
54
+ const firstName = children[0].text();
55
+ if (firstName === 'fileInit') {
56
+ // 检查是否被 as 重命名
57
+ if (children.length > 1) {
58
+ const asNode = children.find((c: AstNode) => c.text() === 'as');
59
+ if (asNode) {
60
+ return err({
61
+ filePath: '',
62
+ type: 'import-violation',
63
+ details: `fileInit 被重命名为 ${children[children.length - 1].text()},违反导入规范`,
64
+ });
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ return ok(true);
73
+ }
74
+
75
+ // ==================== 变量名规范检查 ====================
76
+
77
+ /**
78
+ * 检查 fileInit() 返回值是否被命名为 fileId(违反规范则报错)
79
+ */
80
+ function checkVariableNameConvention(root: AstNode): Result<boolean, CheckIssue> {
81
+ // 查找所有变量声明
82
+ const varDeclarations = findAllByKind(root, 'variable_declarator');
83
+
84
+ for (const decl of varDeclarations) {
85
+ const children = decl.children();
86
+ if (children.length < 2) continue;
87
+
88
+ // 检查初始化值是否是 fileInit 调用
89
+ const initPart = children[children.length - 1];
90
+ if (initPart?.kind() === 'call_expression') {
91
+ const callChildren = initPart.children();
92
+ if (callChildren.length > 0 && callChildren[0].text() === 'fileInit') {
93
+ // 获取变量名
94
+ const varName = children[0].text();
95
+ if (varName !== 'fileId') {
96
+ return err({
97
+ filePath: '',
98
+ type: 'variable-name-violation',
99
+ details: `fileInit 的返回值被命名为 ${varName},应该命名为 fileId`,
100
+ });
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ return ok(true);
107
+ }
108
+
109
+ // ==================== Key 唯一性检查 ====================
110
+
111
+ /**
112
+ * 提取代码中所有 fileId 调用的 key 和 tags
113
+ */
114
+ function collectFileIdUsages(root: AstNode): KeyUsage[] {
115
+ const usages: KeyUsage[] = [];
116
+ const allCalls = findFunctionCalls(root, 'fileId');
117
+
118
+ for (const call of allCalls) {
119
+ const usage = extractKeyFromCall(call, 'fileId');
120
+ if (usage) {
121
+ usages.push(usage);
122
+ }
123
+ }
124
+
125
+ return usages;
126
+ }
127
+
128
+ // ==================== 文件检测 ====================
129
+
130
+ /**
131
+ * 检查单个文件:验证导入规范、变量名规范、key 唯一性
132
+ */
133
+ function checkFile(filePath: string, root: AstNode): Result<CheckIssue | null, Error> {
134
+ // 步骤 1:导入约束检查
135
+ const importCheck = checkImportConvention(root);
136
+ if (importCheck.isErr()) {
137
+ return ok({
138
+ ...importCheck.error,
139
+ filePath,
140
+ });
141
+ }
142
+
143
+ // 步骤 2:变量名约束检查
144
+ const varNameCheck = checkVariableNameConvention(root);
145
+ if (varNameCheck.isErr()) {
146
+ return ok({
147
+ ...varNameCheck.error,
148
+ filePath,
149
+ });
150
+ }
151
+
152
+ // 步骤 3:Key 唯一性检查
153
+ const idUsages = collectFileIdUsages(root);
154
+ if (idUsages.length === 0) {
155
+ return ok(null);
156
+ }
157
+
158
+ const keyCountMap = new Map<string, number>();
159
+ for (const usage of idUsages) {
160
+ const count = keyCountMap.get(usage.key) ?? 0;
161
+ keyCountMap.set(usage.key, count + 1);
162
+ }
163
+
164
+ const duplicateKeys = Array.from(keyCountMap.entries()).filter(
165
+ ([, count]) => count > 1,
166
+ );
167
+
168
+ if (duplicateKeys.length > 0) {
169
+ return ok({
170
+ filePath,
171
+ type: 'duplicate',
172
+ details: 'Key 存在重复',
173
+ keys: duplicateKeys.map(([key, count]) => ({ key, count })),
174
+ });
175
+ }
176
+
177
+ return ok(null);
178
+ }
179
+
180
+ // ==================== 批量检测 ====================
181
+
182
+ /**
183
+ * 并行检测所有文件(使用 Eff.allSettled)
184
+ */
185
+ function checkAllFiles(allFiles: string[], parentSignal: AbortSignal): ResultAsync<CheckIssue[], Error> {
186
+ if (allFiles.length === 0) {
187
+ return Eff.root(() => Promise.resolve(ok([])));
188
+ }
189
+
190
+ // 直接传递 executor 函数给 Eff.allSettled,而不是 Eff.root 的返回值
191
+ const executors = allFiles.map((file) =>
192
+ async ({ signal }: EffContext) => {
193
+ if (signal.aborted) return err(new Error('任务已取消'));
194
+
195
+ fileId('checkProgress').info(`[正在扫描] ${file}`);
196
+
197
+ const parseResult = await parseFileAsync(file, signal);
198
+ if (parseResult.isErr()) {
199
+ return ok(null as CheckIssue | null);
200
+ }
201
+
202
+ const { root } = parseResult.value;
203
+ const checkResult = checkFile(file, root);
204
+ if (checkResult.isErr()) {
205
+ return ok(null as CheckIssue | null);
206
+ }
207
+
208
+ return ok(checkResult.value);
209
+ },
210
+ ) as unknown as NonEmptyArray<(ctx: EffContext) => Promise<Result<CheckIssue | null, Error>>>;
211
+
212
+ // 使用 Eff.allSettled 并行执行,支持 signal 传播
213
+ return Eff.allSettled(executors, parentSignal).map((results) => {
214
+ const issues: CheckIssue[] = [];
215
+ for (const result of results) {
216
+ if (result.isOk() && result.value !== null) {
217
+ issues.push(result.value);
218
+ }
219
+ }
220
+ return issues;
221
+ });
222
+ }
223
+
224
+ // ==================== 输出格式 ====================
225
+
226
+ /**
227
+ * 分类并格式化输出检测结果
228
+ */
229
+ function printAllIssues(issues: CheckIssue[]): void {
230
+ if (issues.length === 0) {
231
+ return;
232
+ }
233
+
234
+ fileId('summaryHeader').info(
235
+ `\n${'='.repeat(60)}\n检测结果汇总\n${'='.repeat(60)}`,
236
+ );
237
+
238
+ // 分类统计
239
+ const importViolations = issues.filter((i) => i.type === 'import-violation');
240
+ const varNameViolations = issues.filter((i) => i.type === 'variable-name-violation');
241
+ const duplicateIssues = issues.filter((i) => i.type === 'duplicate');
242
+
243
+ // 导入约束违反
244
+ if (importViolations.length > 0) {
245
+ fileId('importViolationsHeader').error(`\n❌ 导入约束违反 (${importViolations.length} 个)`);
246
+ for (const issue of importViolations) {
247
+ fileId('importViolationsPath').error(` ${issue.filePath}`);
248
+ fileId('importViolationsDetail').error(` └─ ${issue.details}`);
249
+ }
250
+ }
251
+
252
+ // 变量名约束违反
253
+ if (varNameViolations.length > 0) {
254
+ fileId('varNameViolationsHeader').error(`\n❌ 变量名约束违反 (${varNameViolations.length} 个)`);
255
+ for (const issue of varNameViolations) {
256
+ fileId('varNameViolationsPath').error(` ${issue.filePath}`);
257
+ fileId('varNameViolationsDetail').error(` └─ ${issue.details}`);
258
+ }
259
+ }
260
+
261
+ // Key 重复
262
+ if (duplicateIssues.length > 0) {
263
+ fileId('duplicateKeysHeader').error(`\n❌ 重复的 Key (${duplicateIssues.length} 个)`);
264
+ for (const issue of duplicateIssues) {
265
+ fileId('duplicateKeysPath').error(` ${issue.filePath}`);
266
+ if (issue.keys) {
267
+ for (const k of issue.keys) {
268
+ fileId('duplicateKeysItem').error(` ├─ ${k.key} (${k.count} 次)`);
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ fileId('summaryFooter').info(`${'='.repeat(60)}\n`);
275
+ }
276
+
277
+ // ==================== 主函数 ====================
278
+
279
+ export const tz = () => {
280
+ return Eff.root(async ({ signal }) => {
281
+ try {
282
+ const allFiles = await scanTypeScriptFilesAsync();
283
+ fileId('checkStart').info(`开始检测 kv2 keys... (共 ${allFiles.length} 个文件)\n`);
284
+
285
+ // 传递 signal 以支持取消传播
286
+ const issuesResult = await checkAllFiles(allFiles, signal);
287
+
288
+ if (issuesResult.isErr()) {
289
+ return err(issuesResult.error);
290
+ }
291
+
292
+ const issues = issuesResult.value;
293
+ printAllIssues(issues);
294
+
295
+ const issueCount = issues.length;
296
+ if (issueCount > 0) {
297
+ fileId('checkCompleteFail').error(
298
+ `\n❌ 检测完成: 发现 ${issueCount} 个问题`,
299
+ );
300
+ process.exit(1);
301
+ } else {
302
+ fileId('checkCompleteSuccess').info(
303
+ '\n✅ 检测完成: 无问题',
304
+ );
305
+ }
306
+
307
+ return ok(undefined);
308
+ } catch (e) {
309
+ return err(e instanceof Error ? e : new Error(String(e)));
310
+ }
311
+ });
312
+ };
313
+
314
+ tz().match(
315
+ () => { /* success */ },
316
+ (err) => {
317
+ fileId('scriptFailed').error(`脚本执行失败: ${err.message}`);
318
+ process.exit(1);
319
+ },
320
+ );