@spaceflow/core 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +1176 -0
  2. package/README.md +105 -0
  3. package/nest-cli.json +10 -0
  4. package/package.json +128 -0
  5. package/rspack.config.mjs +62 -0
  6. package/src/__mocks__/@opencode-ai/sdk.js +9 -0
  7. package/src/__mocks__/c12.ts +3 -0
  8. package/src/app.module.ts +18 -0
  9. package/src/config/ci.config.ts +29 -0
  10. package/src/config/config-loader.ts +101 -0
  11. package/src/config/config-reader.module.ts +16 -0
  12. package/src/config/config-reader.service.ts +133 -0
  13. package/src/config/feishu.config.ts +35 -0
  14. package/src/config/git-provider.config.ts +29 -0
  15. package/src/config/index.ts +29 -0
  16. package/src/config/llm.config.ts +110 -0
  17. package/src/config/schema-generator.service.ts +129 -0
  18. package/src/config/spaceflow.config.ts +292 -0
  19. package/src/config/storage.config.ts +33 -0
  20. package/src/extension-system/extension.interface.ts +221 -0
  21. package/src/extension-system/index.ts +1 -0
  22. package/src/index.ts +80 -0
  23. package/src/locales/en/translation.json +11 -0
  24. package/src/locales/zh-cn/translation.json +11 -0
  25. package/src/shared/claude-setup/claude-setup.module.ts +8 -0
  26. package/src/shared/claude-setup/claude-setup.service.ts +131 -0
  27. package/src/shared/claude-setup/index.ts +2 -0
  28. package/src/shared/editor-config/index.ts +23 -0
  29. package/src/shared/feishu-sdk/feishu-sdk.module.ts +77 -0
  30. package/src/shared/feishu-sdk/feishu-sdk.service.ts +130 -0
  31. package/src/shared/feishu-sdk/fieshu-card.service.ts +139 -0
  32. package/src/shared/feishu-sdk/index.ts +4 -0
  33. package/src/shared/feishu-sdk/types/card-action.ts +132 -0
  34. package/src/shared/feishu-sdk/types/card.ts +64 -0
  35. package/src/shared/feishu-sdk/types/common.ts +22 -0
  36. package/src/shared/feishu-sdk/types/index.ts +46 -0
  37. package/src/shared/feishu-sdk/types/message.ts +35 -0
  38. package/src/shared/feishu-sdk/types/module.ts +21 -0
  39. package/src/shared/feishu-sdk/types/user.ts +77 -0
  40. package/src/shared/git-provider/adapters/gitea.adapter.spec.ts +473 -0
  41. package/src/shared/git-provider/adapters/gitea.adapter.ts +499 -0
  42. package/src/shared/git-provider/adapters/github.adapter.spec.ts +341 -0
  43. package/src/shared/git-provider/adapters/github.adapter.ts +830 -0
  44. package/src/shared/git-provider/adapters/gitlab.adapter.ts +839 -0
  45. package/src/shared/git-provider/adapters/index.ts +3 -0
  46. package/src/shared/git-provider/detect-provider.spec.ts +195 -0
  47. package/src/shared/git-provider/detect-provider.ts +112 -0
  48. package/src/shared/git-provider/git-provider.interface.ts +188 -0
  49. package/src/shared/git-provider/git-provider.module.ts +73 -0
  50. package/src/shared/git-provider/git-provider.service.spec.ts +282 -0
  51. package/src/shared/git-provider/git-provider.service.ts +309 -0
  52. package/src/shared/git-provider/index.ts +7 -0
  53. package/src/shared/git-provider/parse-repo-url.spec.ts +221 -0
  54. package/src/shared/git-provider/parse-repo-url.ts +155 -0
  55. package/src/shared/git-provider/types.ts +434 -0
  56. package/src/shared/git-sdk/git-sdk-diff.utils.spec.ts +344 -0
  57. package/src/shared/git-sdk/git-sdk-diff.utils.ts +151 -0
  58. package/src/shared/git-sdk/git-sdk.module.ts +8 -0
  59. package/src/shared/git-sdk/git-sdk.service.ts +235 -0
  60. package/src/shared/git-sdk/git-sdk.types.ts +25 -0
  61. package/src/shared/git-sdk/index.ts +4 -0
  62. package/src/shared/i18n/i18n.spec.ts +96 -0
  63. package/src/shared/i18n/i18n.ts +86 -0
  64. package/src/shared/i18n/index.ts +1 -0
  65. package/src/shared/i18n/locale-detect.ts +134 -0
  66. package/src/shared/llm-jsonput/index.ts +94 -0
  67. package/src/shared/llm-jsonput/types.ts +17 -0
  68. package/src/shared/llm-proxy/adapters/claude-code.adapter.spec.ts +131 -0
  69. package/src/shared/llm-proxy/adapters/claude-code.adapter.ts +208 -0
  70. package/src/shared/llm-proxy/adapters/index.ts +4 -0
  71. package/src/shared/llm-proxy/adapters/llm-adapter.interface.ts +23 -0
  72. package/src/shared/llm-proxy/adapters/open-code.adapter.ts +342 -0
  73. package/src/shared/llm-proxy/adapters/openai.adapter.spec.ts +215 -0
  74. package/src/shared/llm-proxy/adapters/openai.adapter.ts +153 -0
  75. package/src/shared/llm-proxy/index.ts +6 -0
  76. package/src/shared/llm-proxy/interfaces/config.interface.ts +32 -0
  77. package/src/shared/llm-proxy/interfaces/index.ts +4 -0
  78. package/src/shared/llm-proxy/interfaces/message.interface.ts +48 -0
  79. package/src/shared/llm-proxy/interfaces/session.interface.ts +28 -0
  80. package/src/shared/llm-proxy/llm-proxy.module.ts +140 -0
  81. package/src/shared/llm-proxy/llm-proxy.service.spec.ts +303 -0
  82. package/src/shared/llm-proxy/llm-proxy.service.ts +132 -0
  83. package/src/shared/llm-proxy/llm-session.spec.ts +111 -0
  84. package/src/shared/llm-proxy/llm-session.ts +109 -0
  85. package/src/shared/llm-proxy/stream-logger.ts +97 -0
  86. package/src/shared/logger/index.ts +11 -0
  87. package/src/shared/logger/logger.interface.ts +93 -0
  88. package/src/shared/logger/logger.spec.ts +178 -0
  89. package/src/shared/logger/logger.ts +175 -0
  90. package/src/shared/logger/renderers/plain.renderer.ts +116 -0
  91. package/src/shared/logger/renderers/tui.renderer.ts +162 -0
  92. package/src/shared/mcp/index.ts +332 -0
  93. package/src/shared/output/index.ts +2 -0
  94. package/src/shared/output/output.module.ts +9 -0
  95. package/src/shared/output/output.service.ts +97 -0
  96. package/src/shared/package-manager/index.ts +115 -0
  97. package/src/shared/parallel/index.ts +1 -0
  98. package/src/shared/parallel/parallel-executor.ts +169 -0
  99. package/src/shared/rspack-config/index.ts +1 -0
  100. package/src/shared/rspack-config/rspack-config.ts +157 -0
  101. package/src/shared/source-utils/index.ts +130 -0
  102. package/src/shared/spaceflow-dir/index.ts +158 -0
  103. package/src/shared/storage/adapters/file.adapter.ts +113 -0
  104. package/src/shared/storage/adapters/index.ts +3 -0
  105. package/src/shared/storage/adapters/memory.adapter.ts +50 -0
  106. package/src/shared/storage/adapters/storage-adapter.interface.ts +48 -0
  107. package/src/shared/storage/index.ts +4 -0
  108. package/src/shared/storage/storage.module.ts +150 -0
  109. package/src/shared/storage/storage.service.ts +293 -0
  110. package/src/shared/storage/types.ts +51 -0
  111. package/src/shared/verbose/index.ts +73 -0
  112. package/test/app.e2e-spec.ts +22 -0
  113. package/tsconfig.build.json +4 -0
  114. package/tsconfig.json +25 -0
  115. package/tsconfig.skill.json +18 -0
  116. package/vitest.config.ts +58 -0
@@ -0,0 +1,97 @@
1
+ import type { LlmStreamEvent } from "./interfaces";
2
+
3
+ export interface StreamLoggerState {
4
+ isFirstText: boolean;
5
+ }
6
+
7
+ /**
8
+ * 创建一个新的 StreamLogger 状态
9
+ */
10
+ export function createStreamLoggerState(): StreamLoggerState {
11
+ return { isFirstText: true };
12
+ }
13
+
14
+ /**
15
+ * 记录 LLM 流式事件到终端
16
+ * @param event LLM 流式事件
17
+ * @param state 日志状态(用于跟踪是否是第一个文本块)
18
+ */
19
+ export function logStreamEvent(event: LlmStreamEvent, state: StreamLoggerState): void {
20
+ switch (event.type) {
21
+ case "text":
22
+ if (state.isFirstText) {
23
+ process.stdout.write("\n🤖 AI: ");
24
+ state.isFirstText = false;
25
+ }
26
+ process.stdout.write(event.content);
27
+ break;
28
+ case "tool_use":
29
+ console.log(`\n🛠️ 工具调用: ${event.name}`);
30
+ if (event.title) {
31
+ console.log(` 标题: ${event.title}`);
32
+ }
33
+ console.log(` 输入: ${JSON.stringify(event.input)}`);
34
+ if (event.status) {
35
+ console.log(` 状态: ${event.status}`);
36
+ }
37
+ if (event.output) {
38
+ console.log(
39
+ ` 输出: ${event.output.substring(0, 200)}${event.output.length > 200 ? "..." : ""}`,
40
+ );
41
+ }
42
+ state.isFirstText = true;
43
+ break;
44
+ case "thought":
45
+ console.log(`\n💭 思考: ${event.content}`);
46
+ state.isFirstText = true;
47
+ break;
48
+ case "result":
49
+ console.log(`\n✅ 结果已返回`);
50
+ state.isFirstText = true;
51
+ break;
52
+ case "error":
53
+ console.error(`\n❌ 错误: ${event.message}`);
54
+ state.isFirstText = true;
55
+ break;
56
+ case "agent":
57
+ console.log(`\n🤖 子代理: ${event.name}`);
58
+ if (event.source) {
59
+ console.log(
60
+ ` 来源: ${event.source.substring(0, 100)}${event.source.length > 100 ? "..." : ""}`,
61
+ );
62
+ }
63
+ state.isFirstText = true;
64
+ break;
65
+ case "subtask":
66
+ console.log(`\n📋 子任务: ${event.description}`);
67
+ console.log(` 代理: ${event.agent}`);
68
+ console.log(
69
+ ` 提示: ${event.prompt.substring(0, 100)}${event.prompt.length > 100 ? "..." : ""}`,
70
+ );
71
+ state.isFirstText = true;
72
+ break;
73
+ case "step_start":
74
+ console.log(`\n▶️ 步骤开始`);
75
+ state.isFirstText = true;
76
+ break;
77
+ case "step_finish":
78
+ console.log(`\n⏹️ 步骤结束: ${event.reason}`);
79
+ if (event.tokens) {
80
+ const tokens = event.tokens as any;
81
+ console.log(
82
+ ` Token: 输入=${tokens.input || 0}, 输出=${tokens.output || 0}, 推理=${tokens.reasoning || 0}`,
83
+ );
84
+ }
85
+ if (event.cost !== undefined) {
86
+ console.log(` 成本: $${event.cost.toFixed(6)}`);
87
+ }
88
+ state.isFirstText = true;
89
+ break;
90
+ case "reasoning":
91
+ console.log(
92
+ `\n🧠 推理: ${event.content.substring(0, 200)}${event.content.length > 200 ? "..." : ""}`,
93
+ );
94
+ state.isFirstText = true;
95
+ break;
96
+ }
97
+ }
@@ -0,0 +1,11 @@
1
+ export { Logger } from "./logger";
2
+ export type {
3
+ LoggerOptions,
4
+ LogLevel,
5
+ RenderMode,
6
+ Spinner,
7
+ ProgressBar,
8
+ ProgressBarOptions,
9
+ TaskItem,
10
+ TaskControl,
11
+ } from "./logger.interface";
@@ -0,0 +1,93 @@
1
+ import type { LogLevel } from "../verbose";
2
+ import { LOG_LEVEL_PRIORITY } from "../verbose";
3
+
4
+ export type { LogLevel };
5
+ export { LOG_LEVEL_PRIORITY };
6
+
7
+ /** 渲染模式 */
8
+ export type RenderMode = "plain" | "tui" | "auto";
9
+
10
+ /** Logger 配置 */
11
+ export interface LoggerOptions {
12
+ /** 日志所属命名空间(通常为命令名) */
13
+ readonly name: string;
14
+ /** 输出模式,默认 "auto"(TTY=tui,CI/管道=plain) */
15
+ readonly mode?: RenderMode;
16
+ /** 日志级别,默认 "info" */
17
+ readonly level?: LogLevel;
18
+ }
19
+
20
+ /** Spinner 控制接口 */
21
+ export interface Spinner {
22
+ /** 更新 spinner 文本 */
23
+ update(message: string): void;
24
+ /** 成功结束 */
25
+ succeed(message?: string): void;
26
+ /** 失败结束 */
27
+ fail(message?: string): void;
28
+ /** 静默停止 */
29
+ stop(): void;
30
+ }
31
+
32
+ /** 进度条控制接口 */
33
+ export interface ProgressBar {
34
+ /** 更新进度 */
35
+ update(current: number, message?: string): void;
36
+ /** 完成 */
37
+ finish(message?: string): void;
38
+ }
39
+
40
+ /** 进度条配置 */
41
+ export interface ProgressBarOptions {
42
+ /** 总数 */
43
+ readonly total: number;
44
+ /** 标签 */
45
+ readonly label?: string;
46
+ /** 进度条宽度(字符数),默认 30 */
47
+ readonly width?: number;
48
+ }
49
+
50
+ /** 任务控制接口 */
51
+ export interface TaskControl {
52
+ /** 更新任务状态文本 */
53
+ update(message: string): void;
54
+ /** 跳过任务 */
55
+ skip(reason?: string): void;
56
+ }
57
+
58
+ /** 任务项定义 */
59
+ export interface TaskItem<T = void> {
60
+ /** 任务标题 */
61
+ readonly title: string;
62
+ /** 任务执行函数 */
63
+ readonly task: (ctx: T, control: TaskControl) => Promise<T>;
64
+ /** 是否启用,默认 true */
65
+ readonly enabled?: boolean;
66
+ }
67
+
68
+ /** 任务执行结果 */
69
+ export type TaskStatus = "pending" | "running" | "success" | "failed" | "skipped";
70
+
71
+ /** 渲染器接口(策略模式) */
72
+ export interface LogRenderer {
73
+ /** 输出 info 级别日志 */
74
+ info(prefix: string, message: string): void;
75
+ /** 输出 success 级别日志 */
76
+ success(prefix: string, message: string): void;
77
+ /** 输出 warn 级别日志 */
78
+ warn(prefix: string, message: string): void;
79
+ /** 输出 error 级别日志 */
80
+ error(prefix: string, message: string): void;
81
+ /** 输出 debug 级别日志 */
82
+ debug(prefix: string, message: string): void;
83
+ /** 输出 verbose 级别日志 */
84
+ verbose(prefix: string, message: string): void;
85
+ /** 创建 Spinner */
86
+ createSpinner(prefix: string, message: string): Spinner;
87
+ /** 创建进度条 */
88
+ createProgressBar(prefix: string, options: ProgressBarOptions): ProgressBar;
89
+ /** 执行任务列表 */
90
+ runTasks<T>(prefix: string, items: TaskItem<T>[]): Promise<T[]>;
91
+ /** 输出表格 */
92
+ table(prefix: string, data: Record<string, unknown>[]): void;
93
+ }
@@ -0,0 +1,178 @@
1
+ import { vi, type MockInstance } from "vitest";
2
+ import { Logger } from "./logger";
3
+
4
+ describe("Logger", () => {
5
+ let consoleSpy: {
6
+ log: MockInstance;
7
+ warn: MockInstance;
8
+ error: MockInstance;
9
+ };
10
+
11
+ beforeEach(() => {
12
+ consoleSpy = {
13
+ log: vi.spyOn(console, "log").mockImplementation(),
14
+ warn: vi.spyOn(console, "warn").mockImplementation(),
15
+ error: vi.spyOn(console, "error").mockImplementation(),
16
+ };
17
+ });
18
+
19
+ afterEach(() => {
20
+ vi.restoreAllMocks();
21
+ });
22
+
23
+ describe("基础日志", () => {
24
+ it("info 输出包含前缀和消息", () => {
25
+ const logger = new Logger({ name: "test", mode: "plain" });
26
+ logger.info("hello");
27
+ expect(consoleSpy.log).toHaveBeenCalledTimes(1);
28
+ const output = consoleSpy.log.mock.calls[0][0] as string;
29
+ expect(output).toContain("[test]");
30
+ expect(output).toContain("hello");
31
+ });
32
+
33
+ it("success 输出包含 ✅", () => {
34
+ const logger = new Logger({ name: "test", mode: "plain" });
35
+ logger.success("done");
36
+ const output = consoleSpy.log.mock.calls[0][0] as string;
37
+ expect(output).toContain("✅");
38
+ expect(output).toContain("done");
39
+ });
40
+
41
+ it("warn 使用 console.warn", () => {
42
+ const logger = new Logger({ name: "test", mode: "plain" });
43
+ logger.warn("caution");
44
+ expect(consoleSpy.warn).toHaveBeenCalledTimes(1);
45
+ const output = consoleSpy.warn.mock.calls[0][0] as string;
46
+ expect(output).toContain("caution");
47
+ });
48
+
49
+ it("error 使用 console.error", () => {
50
+ const logger = new Logger({ name: "test", mode: "plain" });
51
+ logger.error("fail");
52
+ expect(consoleSpy.error).toHaveBeenCalledTimes(1);
53
+ const output = consoleSpy.error.mock.calls[0][0] as string;
54
+ expect(output).toContain("fail");
55
+ });
56
+ });
57
+
58
+ describe("日志级别", () => {
59
+ it("level=info 时 verbose 不输出", () => {
60
+ const logger = new Logger({ name: "test", mode: "plain", level: "info" });
61
+ logger.verbose("detail");
62
+ expect(consoleSpy.log).not.toHaveBeenCalled();
63
+ });
64
+
65
+ it("level=verbose 时 verbose 输出", () => {
66
+ const logger = new Logger({ name: "test", mode: "plain", level: "verbose" });
67
+ logger.verbose("detail");
68
+ expect(consoleSpy.log).toHaveBeenCalledTimes(1);
69
+ });
70
+
71
+ it("level=verbose 时 debug 不输出", () => {
72
+ const logger = new Logger({ name: "test", mode: "plain", level: "verbose" });
73
+ logger.debug("trace");
74
+ expect(consoleSpy.log).not.toHaveBeenCalled();
75
+ });
76
+
77
+ it("level=debug 时 debug 输出", () => {
78
+ const logger = new Logger({ name: "test", mode: "plain", level: "debug" });
79
+ logger.debug("trace");
80
+ expect(consoleSpy.log).toHaveBeenCalledTimes(1);
81
+ const output = consoleSpy.log.mock.calls[0][0] as string;
82
+ expect(output).toContain("[DEBUG]");
83
+ });
84
+
85
+ it("level=silent 时所有日志不输出", () => {
86
+ const logger = new Logger({ name: "test", mode: "plain", level: "silent" });
87
+ logger.info("a");
88
+ logger.success("b");
89
+ logger.warn("c");
90
+ logger.error("d");
91
+ logger.verbose("e");
92
+ logger.debug("f");
93
+ expect(consoleSpy.log).not.toHaveBeenCalled();
94
+ expect(consoleSpy.warn).not.toHaveBeenCalled();
95
+ expect(consoleSpy.error).not.toHaveBeenCalled();
96
+ });
97
+ });
98
+
99
+ describe("字符串构造", () => {
100
+ it("支持字符串参数快捷创建", () => {
101
+ const logger = new Logger("build");
102
+ logger.info("start");
103
+ // 不报错即可,auto 模式下可能是 plain 或 tui
104
+ expect(consoleSpy.log).toHaveBeenCalled();
105
+ });
106
+ });
107
+
108
+ describe("child", () => {
109
+ it("子 Logger 前缀包含父命名空间", () => {
110
+ const logger = new Logger({ name: "build", mode: "plain" });
111
+ const child = logger.child("compile");
112
+ child.info("processing");
113
+ const output = consoleSpy.log.mock.calls[0][0] as string;
114
+ expect(output).toContain("[build:compile]");
115
+ });
116
+ });
117
+
118
+ describe("Spinner (plain 模式)", () => {
119
+ it("spin 输出开始消息", () => {
120
+ const logger = new Logger({ name: "test", mode: "plain" });
121
+ const spinner = logger.spin("loading");
122
+ expect(consoleSpy.log).toHaveBeenCalledTimes(1);
123
+ const output = consoleSpy.log.mock.calls[0][0] as string;
124
+ expect(output).toContain("loading");
125
+ spinner.succeed("loaded");
126
+ expect(consoleSpy.log).toHaveBeenCalledTimes(2);
127
+ });
128
+ });
129
+
130
+ describe("ProgressBar (plain 模式)", () => {
131
+ it("progress 输出进度信息", () => {
132
+ const logger = new Logger({ name: "test", mode: "plain" });
133
+ const bar = logger.progress({ total: 10, label: "files" });
134
+ expect(consoleSpy.log).toHaveBeenCalledTimes(1);
135
+ bar.update(5);
136
+ expect(consoleSpy.log).toHaveBeenCalledTimes(2);
137
+ const output = consoleSpy.log.mock.calls[1][0] as string;
138
+ expect(output).toContain("50%");
139
+ bar.finish();
140
+ expect(consoleSpy.log).toHaveBeenCalledTimes(3);
141
+ });
142
+ });
143
+
144
+ describe("Tasks (plain 模式)", () => {
145
+ it("顺序执行任务并输出状态", async () => {
146
+ const logger = new Logger({ name: "test", mode: "plain" });
147
+ const results = await logger.tasks([
148
+ { title: "步骤1", task: async () => "a" as never },
149
+ { title: "步骤2", task: async () => "b" as never },
150
+ ]);
151
+ expect(results).toHaveLength(2);
152
+ const allOutput = consoleSpy.log.mock.calls.map((c: unknown[]) => c[0]).join("\n");
153
+ expect(allOutput).toContain("步骤1");
154
+ expect(allOutput).toContain("步骤2");
155
+ });
156
+
157
+ it("任务失败时抛出错误", async () => {
158
+ const logger = new Logger({ name: "test", mode: "plain" });
159
+ await expect(
160
+ logger.tasks([
161
+ {
162
+ title: "会失败",
163
+ task: async () => {
164
+ throw new Error("boom");
165
+ },
166
+ },
167
+ ]),
168
+ ).rejects.toThrow("boom");
169
+ });
170
+
171
+ it("enabled=false 的任务被跳过", async () => {
172
+ const logger = new Logger({ name: "test", mode: "plain" });
173
+ const taskFn = vi.fn();
174
+ await logger.tasks([{ title: "跳过", task: taskFn, enabled: false }]);
175
+ expect(taskFn).not.toHaveBeenCalled();
176
+ });
177
+ });
178
+ });
@@ -0,0 +1,175 @@
1
+ import type {
2
+ LoggerOptions,
3
+ LogLevel,
4
+ RenderMode,
5
+ LogRenderer,
6
+ Spinner,
7
+ ProgressBar,
8
+ ProgressBarOptions,
9
+ TaskItem,
10
+ } from "./logger.interface";
11
+ import { LOG_LEVEL_PRIORITY } from "./logger.interface";
12
+ import { PlainRenderer } from "./renderers/plain.renderer";
13
+
14
+ /** 检测是否为 TUI 环境 */
15
+ const detectMode = (): RenderMode => {
16
+ if (process.env.CI || !process.stdout.isTTY) return "plain";
17
+ return "tui";
18
+ };
19
+
20
+ /** TUI 渲染器缓存(延迟加载,避免 ESM 兼容问题) */
21
+ let tuiRendererCache: LogRenderer | null = null;
22
+
23
+ /** 延迟加载 TUI 渲染器(chalk/ora/log-update 均为纯 ESM 包) */
24
+ const loadTuiRenderer = async (): Promise<LogRenderer> => {
25
+ if (tuiRendererCache) return tuiRendererCache;
26
+ const { TuiRenderer } = await import("./renderers/tui.renderer");
27
+ tuiRendererCache = new TuiRenderer();
28
+ return tuiRendererCache;
29
+ };
30
+
31
+ /** 解析渲染模式,返回实际模式标识 */
32
+ const resolveMode = (mode: RenderMode): "plain" | "tui" => {
33
+ if (mode === "auto") return detectMode() === "tui" ? "tui" : "plain";
34
+ return mode;
35
+ };
36
+
37
+ /**
38
+ * 全局日志工具类
39
+ *
40
+ * 每个子命令创建独立实例,支持 plain / tui 两种输出模式。
41
+ * TUI 模式提供 Spinner、进度条、任务列表等富交互能力,
42
+ * plain 模式下自动降级为普通文本输出。
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * const logger = new Logger("build");
47
+ * logger.info("开始构建");
48
+ * const s = logger.spin("编译中...");
49
+ * s.succeed("编译完成");
50
+ * ```
51
+ */
52
+ export class Logger {
53
+ private readonly name: string;
54
+ private readonly level: LogLevel;
55
+ private readonly resolvedMode: "plain" | "tui";
56
+ private readonly plainRenderer: PlainRenderer;
57
+ private tuiRenderer: LogRenderer | null = null;
58
+
59
+ constructor(options: LoggerOptions | string) {
60
+ const opts = typeof options === "string" ? { name: options } : options;
61
+ this.name = opts.name;
62
+ this.level = opts.level ?? "info";
63
+ this.resolvedMode = resolveMode(opts.mode ?? "auto");
64
+ this.plainRenderer = new PlainRenderer();
65
+ }
66
+
67
+ /** 格式化前缀 */
68
+ private get prefix(): string {
69
+ return `[${this.name}]`;
70
+ }
71
+
72
+ /** 获取当前渲染器(TUI 未加载时降级为 plain) */
73
+ private get renderer(): LogRenderer {
74
+ if (this.resolvedMode === "tui" && this.tuiRenderer) return this.tuiRenderer;
75
+ return this.plainRenderer;
76
+ }
77
+
78
+ /**
79
+ * 初始化 TUI 渲染器(异步)
80
+ * 在使用 TUI 特有功能前调用,确保渲染器已加载
81
+ */
82
+ async init(): Promise<void> {
83
+ if (this.resolvedMode === "tui" && !this.tuiRenderer) {
84
+ this.tuiRenderer = await loadTuiRenderer();
85
+ }
86
+ }
87
+
88
+ /** 判断是否应输出指定级别的日志 */
89
+ private shouldLog(level: LogLevel): boolean {
90
+ return LOG_LEVEL_PRIORITY[level] <= LOG_LEVEL_PRIORITY[this.level];
91
+ }
92
+
93
+ /** 输出 info 级别日志 */
94
+ info(message: string): void {
95
+ if (this.shouldLog("info")) {
96
+ this.renderer.info(this.prefix, message);
97
+ }
98
+ }
99
+
100
+ /** 输出 success 级别日志 */
101
+ success(message: string): void {
102
+ if (this.shouldLog("info")) {
103
+ this.renderer.success(this.prefix, message);
104
+ }
105
+ }
106
+
107
+ /** 输出 warn 级别日志 */
108
+ warn(message: string): void {
109
+ if (this.shouldLog("info")) {
110
+ this.renderer.warn(this.prefix, message);
111
+ }
112
+ }
113
+
114
+ /** 输出 error 级别日志 */
115
+ error(message: string): void {
116
+ if (this.shouldLog("info")) {
117
+ this.renderer.error(this.prefix, message);
118
+ }
119
+ }
120
+
121
+ /** 输出 verbose 级别日志(level >= verbose 才输出) */
122
+ verbose(message: string): void {
123
+ if (this.shouldLog("verbose")) {
124
+ this.renderer.verbose(this.prefix, message);
125
+ }
126
+ }
127
+
128
+ /** 输出 debug 级别日志(level >= debug 才输出) */
129
+ debug(message: string): void {
130
+ if (this.shouldLog("debug")) {
131
+ this.renderer.debug(this.prefix, message);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * 创建 Spinner
137
+ * TUI 模式下显示动画 spinner,plain 模式下降级为普通日志
138
+ */
139
+ spin(message: string): Spinner {
140
+ return this.renderer.createSpinner(this.prefix, message);
141
+ }
142
+
143
+ /**
144
+ * 创建进度条
145
+ * TUI 模式下显示实时进度条,plain 模式下降级为百分比日志
146
+ */
147
+ progress(options: ProgressBarOptions): ProgressBar {
148
+ return this.renderer.createProgressBar(this.prefix, options);
149
+ }
150
+
151
+ /**
152
+ * 执行任务列表
153
+ * TUI 模式下多行实时更新,plain 模式下顺序输出
154
+ */
155
+ async tasks<T>(items: TaskItem<T>[]): Promise<T[]> {
156
+ return this.renderer.runTasks<T>(this.prefix, items);
157
+ }
158
+
159
+ /** 输出表格 */
160
+ table(data: Record<string, unknown>[]): void {
161
+ this.renderer.table(this.prefix, data);
162
+ }
163
+
164
+ /**
165
+ * 创建子 Logger
166
+ * 命名空间自动拼接,如 "build" → "build:compile"
167
+ */
168
+ child(name: string): Logger {
169
+ return new Logger({
170
+ name: `${this.name}:${name}`,
171
+ level: this.level,
172
+ mode: this.resolvedMode,
173
+ });
174
+ }
175
+ }
@@ -0,0 +1,116 @@
1
+ import type {
2
+ LogRenderer,
3
+ Spinner,
4
+ ProgressBar,
5
+ ProgressBarOptions,
6
+ TaskItem,
7
+ TaskControl,
8
+ } from "../logger.interface";
9
+
10
+ /** 时间戳格式化 */
11
+ const timestamp = (): string => {
12
+ const now = new Date();
13
+ return `${now.toLocaleTimeString()}`;
14
+ };
15
+
16
+ /** plain 模式渲染器:纯文本输出,适合 CI / 管道 */
17
+ export class PlainRenderer implements LogRenderer {
18
+ info(prefix: string, message: string): void {
19
+ console.log(`${timestamp()} ${prefix} ${message}`);
20
+ }
21
+
22
+ success(prefix: string, message: string): void {
23
+ console.log(`${timestamp()} ${prefix} ✅ ${message}`);
24
+ }
25
+
26
+ warn(prefix: string, message: string): void {
27
+ console.warn(`${timestamp()} ${prefix} ⚠️ ${message}`);
28
+ }
29
+
30
+ error(prefix: string, message: string): void {
31
+ console.error(`${timestamp()} ${prefix} ❌ ${message}`);
32
+ }
33
+
34
+ debug(prefix: string, message: string): void {
35
+ console.log(`${timestamp()} ${prefix} [DEBUG] ${message}`);
36
+ }
37
+
38
+ verbose(prefix: string, message: string): void {
39
+ console.log(`${timestamp()} ${prefix} ${message}`);
40
+ }
41
+
42
+ createSpinner(prefix: string, message: string): Spinner {
43
+ console.log(`${timestamp()} ${prefix} ⏳ ${message}`);
44
+ return {
45
+ update: (msg: string) => {
46
+ console.log(`${timestamp()} ${prefix} ⏳ ${msg}`);
47
+ },
48
+ succeed: (msg?: string) => {
49
+ console.log(`${timestamp()} ${prefix} ✅ ${msg ?? message}`);
50
+ },
51
+ fail: (msg?: string) => {
52
+ console.error(`${timestamp()} ${prefix} ❌ ${msg ?? message}`);
53
+ },
54
+ stop: () => {},
55
+ };
56
+ }
57
+
58
+ createProgressBar(prefix: string, options: ProgressBarOptions): ProgressBar {
59
+ const { total, label = "" } = options;
60
+ const tag = label ? `${label} ` : "";
61
+ console.log(`${timestamp()} ${prefix} ${tag}0/${total}`);
62
+ return {
63
+ update: (current: number, msg?: string) => {
64
+ const pct = Math.round((current / total) * 100);
65
+ const suffix = msg ? ` ${msg}` : "";
66
+ console.log(`${timestamp()} ${prefix} ${tag}${current}/${total} (${pct}%)${suffix}`);
67
+ },
68
+ finish: (msg?: string) => {
69
+ const suffix = msg ? ` ${msg}` : "";
70
+ console.log(`${timestamp()} ${prefix} ✅ ${tag}${total}/${total} (100%)${suffix}`);
71
+ },
72
+ };
73
+ }
74
+
75
+ async runTasks<T>(prefix: string, items: TaskItem<T>[]): Promise<T[]> {
76
+ const results: T[] = [];
77
+ for (const item of items) {
78
+ if (item.enabled === false) {
79
+ console.log(`${timestamp()} ${prefix} ⏭️ [跳过] ${item.title}`);
80
+ continue;
81
+ }
82
+ console.log(`${timestamp()} ${prefix} ▶ ${item.title}`);
83
+ let skipped = false;
84
+ let skipReason = "";
85
+ const control: TaskControl = {
86
+ update: (msg: string) => {
87
+ console.log(`${timestamp()} ${prefix} ${msg}`);
88
+ },
89
+ skip: (reason?: string) => {
90
+ skipped = true;
91
+ skipReason = reason ?? "";
92
+ },
93
+ };
94
+ try {
95
+ const ctx = (results.length > 0 ? results[results.length - 1] : undefined) as T;
96
+ const result = await item.task(ctx, control);
97
+ if (skipped) {
98
+ const suffix = skipReason ? `: ${skipReason}` : "";
99
+ console.log(`${timestamp()} ${prefix} ⏭️ ${item.title}${suffix}`);
100
+ } else {
101
+ console.log(`${timestamp()} ${prefix} ✅ ${item.title}`);
102
+ }
103
+ results.push(result);
104
+ } catch (err) {
105
+ const errMsg = err instanceof Error ? err.message : String(err);
106
+ console.error(`${timestamp()} ${prefix} ❌ ${item.title}: ${errMsg}`);
107
+ throw err;
108
+ }
109
+ }
110
+ return results;
111
+ }
112
+
113
+ table(_prefix: string, data: Record<string, unknown>[]): void {
114
+ console.table(data);
115
+ }
116
+ }