@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,341 @@
1
+ import { vi, type MockInstance } from "vitest";
2
+ import { GithubAdapter } from "./github.adapter";
3
+ import type { GitProviderModuleOptions } from "../types";
4
+
5
+ const mockOptions: GitProviderModuleOptions = {
6
+ provider: "github",
7
+ baseUrl: "https://api.github.com",
8
+ token: "ghp-test-token",
9
+ };
10
+
11
+ /** 构造 mock Response */
12
+ function mockResponse(body: unknown, status = 200): Response {
13
+ const text = typeof body === "string" ? body : JSON.stringify(body);
14
+ return {
15
+ ok: status >= 200 && status < 300,
16
+ status,
17
+ statusText: status === 200 ? "OK" : "Error",
18
+ text: vi.fn().mockResolvedValue(text),
19
+ json: vi.fn().mockResolvedValue(body),
20
+ headers: new Headers(),
21
+ } as unknown as Response;
22
+ }
23
+
24
+ describe("GithubAdapter", () => {
25
+ let adapter: GithubAdapter;
26
+ let fetchSpy: MockInstance;
27
+
28
+ beforeEach(() => {
29
+ adapter = new GithubAdapter(mockOptions);
30
+ fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(mockResponse({}));
31
+ });
32
+
33
+ afterEach(() => {
34
+ fetchSpy.mockRestore();
35
+ });
36
+
37
+ // ============ 配置验证 ============
38
+
39
+ describe("validateConfig", () => {
40
+ it("配置完整时不应抛出异常", () => {
41
+ expect(() => adapter.validateConfig()).not.toThrow();
42
+ });
43
+
44
+ it("缺少 baseUrl 时应抛出异常", () => {
45
+ const a = new GithubAdapter({ ...mockOptions, baseUrl: "" });
46
+ expect(() => a.validateConfig()).toThrow("缺少配置 gitProvider.baseUrl");
47
+ });
48
+
49
+ it("缺少 token 时应抛出异常", () => {
50
+ const a = new GithubAdapter({ ...mockOptions, token: "" });
51
+ expect(() => a.validateConfig()).toThrow("缺少配置 gitProvider.token");
52
+ });
53
+ });
54
+
55
+ // ============ request 基础方法 ============
56
+
57
+ describe("request", () => {
58
+ it("应使用 Bearer 认证和 GitHub Accept header", async () => {
59
+ fetchSpy.mockResolvedValue(mockResponse({ id: 1, name: "repo" }));
60
+ await adapter.getRepository("owner", "repo");
61
+ expect(fetchSpy).toHaveBeenCalledWith(
62
+ "https://api.github.com/repos/owner/repo",
63
+ expect.objectContaining({
64
+ method: "GET",
65
+ headers: expect.objectContaining({
66
+ Authorization: "Bearer ghp-test-token",
67
+ Accept: "application/vnd.github+json",
68
+ "X-GitHub-Api-Version": "2022-11-28",
69
+ }),
70
+ }),
71
+ );
72
+ });
73
+
74
+ it("API 返回错误时应抛出异常", async () => {
75
+ fetchSpy.mockResolvedValue(mockResponse("Not Found", 404));
76
+ await expect(adapter.getRepository("owner", "repo")).rejects.toThrow("GitHub API error: 404");
77
+ });
78
+ });
79
+
80
+ // ============ 仓库操作 ============
81
+
82
+ describe("getRepository", () => {
83
+ it("应正确映射 GitHub 响应", async () => {
84
+ const ghRepo = {
85
+ id: 123,
86
+ name: "my-repo",
87
+ full_name: "owner/my-repo",
88
+ default_branch: "main",
89
+ owner: { id: 1, login: "owner", full_name: "Owner" },
90
+ };
91
+ fetchSpy.mockResolvedValue(mockResponse(ghRepo));
92
+ const result = await adapter.getRepository("owner", "my-repo");
93
+ expect(result.id).toBe(123);
94
+ expect(result.name).toBe("my-repo");
95
+ expect(result.default_branch).toBe("main");
96
+ expect(result.owner?.login).toBe("owner");
97
+ });
98
+ });
99
+
100
+ // ============ 分支操作 ============
101
+
102
+ describe("getBranch", () => {
103
+ it("应正确映射 GitHub 分支响应", async () => {
104
+ const ghBranch = {
105
+ name: "main",
106
+ protected: true,
107
+ commit: { sha: "abc123", commit: { message: "init" } },
108
+ };
109
+ fetchSpy.mockResolvedValue(mockResponse(ghBranch));
110
+ const result = await adapter.getBranch("owner", "repo", "main");
111
+ expect(result.name).toBe("main");
112
+ expect(result.protected).toBe(true);
113
+ expect(result.commit?.id).toBe("abc123");
114
+ });
115
+ });
116
+
117
+ // ============ 分支保护 ============
118
+
119
+ describe("lockBranch", () => {
120
+ it("无白名单时应设置 restrictions", async () => {
121
+ fetchSpy.mockResolvedValue(mockResponse({}));
122
+ await adapter.lockBranch("owner", "repo", "main");
123
+ const body = JSON.parse(fetchSpy.mock.calls[0][1].body);
124
+ expect(body.restrictions).toEqual({ users: [], teams: [] });
125
+ expect(body.enforce_admins).toBe(true);
126
+ });
127
+
128
+ it("有白名单时应设置 restrictions.users", async () => {
129
+ fetchSpy.mockResolvedValue(mockResponse({}));
130
+ await adapter.lockBranch("owner", "repo", "main", {
131
+ pushWhitelistUsernames: ["bot"],
132
+ });
133
+ const body = JSON.parse(fetchSpy.mock.calls[0][1].body);
134
+ expect(body.restrictions.users).toEqual(["bot"]);
135
+ });
136
+ });
137
+
138
+ describe("unlockBranch", () => {
139
+ it("有保护规则时应删除并返回", async () => {
140
+ // getBranchProtection
141
+ fetchSpy.mockResolvedValueOnce(mockResponse({ url: "..." }));
142
+ // deleteBranchProtection
143
+ fetchSpy.mockResolvedValueOnce(mockResponse("", 204));
144
+ const result = await adapter.unlockBranch("owner", "repo", "main");
145
+ expect(result).not.toBeNull();
146
+ expect(fetchSpy.mock.calls[1][1].method).toBe("DELETE");
147
+ });
148
+
149
+ it("无保护规则时应返回 null", async () => {
150
+ fetchSpy.mockResolvedValue(mockResponse("Not Found", 404));
151
+ const result = await adapter.unlockBranch("owner", "repo", "main");
152
+ expect(result).toBeNull();
153
+ });
154
+ });
155
+
156
+ // ============ Pull Request ============
157
+
158
+ describe("getPullRequest", () => {
159
+ it("应正确映射 GitHub PR 响应", async () => {
160
+ const ghPR = {
161
+ id: 1,
162
+ number: 42,
163
+ title: "Fix bug",
164
+ body: "desc",
165
+ state: "open",
166
+ head: {
167
+ ref: "feature",
168
+ sha: "abc",
169
+ repo: { id: 1, name: "r", full_name: "o/r", owner: { id: 1, login: "o" } },
170
+ },
171
+ base: {
172
+ ref: "main",
173
+ sha: "def",
174
+ repo: { id: 1, name: "r", full_name: "o/r", owner: { id: 1, login: "o" } },
175
+ },
176
+ user: { id: 1, login: "author" },
177
+ requested_reviewers: [{ id: 2, login: "reviewer" }],
178
+ requested_teams: [{ id: 10, name: "team-a" }],
179
+ created_at: "2025-01-01",
180
+ merge_commit_sha: "merge123",
181
+ };
182
+ fetchSpy.mockResolvedValue(mockResponse(ghPR));
183
+ const result = await adapter.getPullRequest("owner", "repo", 42);
184
+ expect(result.number).toBe(42);
185
+ expect(result.title).toBe("Fix bug");
186
+ expect(result.head?.ref).toBe("feature");
187
+ expect(result.user?.login).toBe("author");
188
+ expect(result.requested_reviewers?.[0]?.login).toBe("reviewer");
189
+ expect(result.requested_reviewers_teams?.[0]?.name).toBe("team-a");
190
+ expect(result.merge_base).toBe("merge123");
191
+ });
192
+ });
193
+
194
+ describe("listAllPullRequests", () => {
195
+ it("应支持分页获取全部", async () => {
196
+ const page1 = Array.from({ length: 100 }, (_, i) => ({ id: i, number: i }));
197
+ const page2 = [{ id: 100, number: 100 }];
198
+ fetchSpy
199
+ .mockResolvedValueOnce(mockResponse(page1))
200
+ .mockResolvedValueOnce(mockResponse(page2));
201
+ const result = await adapter.listAllPullRequests("o", "r", { state: "all" });
202
+ expect(result).toHaveLength(101);
203
+ expect(fetchSpy).toHaveBeenCalledTimes(2);
204
+ });
205
+ });
206
+
207
+ describe("getPullRequestFiles", () => {
208
+ it("应正确映射文件变更", async () => {
209
+ const ghFiles = [
210
+ { filename: "a.ts", status: "modified", additions: 5, deletions: 2, patch: "@@ ..." },
211
+ ];
212
+ fetchSpy.mockResolvedValue(mockResponse(ghFiles));
213
+ const result = await adapter.getPullRequestFiles("o", "r", 1);
214
+ expect(result[0].filename).toBe("a.ts");
215
+ expect(result[0].additions).toBe(5);
216
+ expect(result[0].patch).toBe("@@ ...");
217
+ });
218
+
219
+ it("应支持分页获取全部文件", async () => {
220
+ const page1 = Array.from({ length: 100 }, (_, i) => ({ filename: `f${i}.ts` }));
221
+ const page2 = [{ filename: "f100.ts" }];
222
+ fetchSpy
223
+ .mockResolvedValueOnce(mockResponse(page1))
224
+ .mockResolvedValueOnce(mockResponse(page2));
225
+ const result = await adapter.getPullRequestFiles("o", "r", 1);
226
+ expect(result).toHaveLength(101);
227
+ });
228
+ });
229
+
230
+ // ============ Commit ============
231
+
232
+ describe("getCommit", () => {
233
+ it("应正确映射 commit 和 files", async () => {
234
+ const ghCommit = {
235
+ sha: "abc123",
236
+ commit: { message: "fix", author: { name: "A", email: "a@b.c", date: "2025-01-01" } },
237
+ author: { id: 1, login: "a" },
238
+ files: [{ filename: "x.ts", status: "modified", additions: 1 }],
239
+ };
240
+ fetchSpy.mockResolvedValue(mockResponse(ghCommit));
241
+ const result = await adapter.getCommit("o", "r", "abc123");
242
+ expect(result.sha).toBe("abc123");
243
+ expect(result.commit?.message).toBe("fix");
244
+ expect(result.files).toHaveLength(1);
245
+ expect(result.files?.[0].filename).toBe("x.ts");
246
+ });
247
+ });
248
+
249
+ // ============ 文件操作 ============
250
+
251
+ describe("getFileContent", () => {
252
+ it("应解码 base64 内容", async () => {
253
+ const content = Buffer.from("hello world").toString("base64");
254
+ fetchSpy.mockResolvedValue(mockResponse({ content, encoding: "base64" }));
255
+ const result = await adapter.getFileContent("o", "r", "a.ts");
256
+ expect(result).toBe("hello world");
257
+ });
258
+
259
+ it("404 时应返回空字符串", async () => {
260
+ fetchSpy.mockResolvedValue(mockResponse("Not Found", 404));
261
+ const result = await adapter.getFileContent("o", "r", "missing.ts");
262
+ expect(result).toBe("");
263
+ });
264
+
265
+ it("指定 ref 时应添加 query 参数", async () => {
266
+ fetchSpy.mockResolvedValue(mockResponse({ content: "", encoding: "base64" }));
267
+ await adapter.getFileContent("o", "r", "a.ts", "dev");
268
+ expect(fetchSpy).toHaveBeenCalledWith(expect.stringContaining("?ref=dev"), expect.anything());
269
+ });
270
+ });
271
+
272
+ // ============ Issue 操作 ============
273
+
274
+ describe("createIssue", () => {
275
+ it("应正确映射 issue 响应", async () => {
276
+ const ghIssue = {
277
+ id: 1,
278
+ number: 10,
279
+ title: "Bug",
280
+ state: "open",
281
+ user: { id: 1, login: "u" },
282
+ html_url: "https://github.com/o/r/issues/10",
283
+ };
284
+ fetchSpy.mockResolvedValue(mockResponse(ghIssue));
285
+ const result = await adapter.createIssue("o", "r", { title: "Bug" });
286
+ expect(result.number).toBe(10);
287
+ expect(result.html_url).toBe("https://github.com/o/r/issues/10");
288
+ });
289
+ });
290
+
291
+ // ============ PR Review ============
292
+
293
+ describe("createPullReview", () => {
294
+ it("应正确映射 review 选项", async () => {
295
+ fetchSpy.mockResolvedValue(mockResponse({ id: 1 }));
296
+ await adapter.createPullReview("o", "r", 1, {
297
+ event: "COMMENT",
298
+ body: "LGTM",
299
+ comments: [{ path: "a.ts", body: "fix", new_position: 10 }],
300
+ });
301
+ const body = JSON.parse(fetchSpy.mock.calls[0][1].body);
302
+ expect(body.event).toBe("COMMENT");
303
+ expect(body.comments[0].position).toBe(10);
304
+ expect(body.comments[0].path).toBe("a.ts");
305
+ });
306
+ });
307
+
308
+ describe("listPullReviews", () => {
309
+ it("应正确映射 review 响应", async () => {
310
+ const ghReviews = [
311
+ {
312
+ id: 1,
313
+ body: "ok",
314
+ state: "APPROVED",
315
+ user: { id: 1, login: "r" },
316
+ submitted_at: "2025-01-01",
317
+ },
318
+ ];
319
+ fetchSpy.mockResolvedValue(mockResponse(ghReviews));
320
+ const result = await adapter.listPullReviews("o", "r", 1);
321
+ expect(result[0].state).toBe("APPROVED");
322
+ expect(result[0].created_at).toBe("2025-01-01");
323
+ });
324
+ });
325
+
326
+ // ============ 用户操作 ============
327
+
328
+ describe("searchUsers", () => {
329
+ it("应请求 /search/users 并解构 items 字段", async () => {
330
+ fetchSpy.mockResolvedValue(mockResponse({ items: [{ id: 1, login: "u", name: "User" }] }));
331
+ const result = await adapter.searchUsers("u", 5);
332
+ expect(result).toHaveLength(1);
333
+ expect(result[0].login).toBe("u");
334
+ expect(result[0].full_name).toBe("User");
335
+ expect(fetchSpy).toHaveBeenCalledWith(
336
+ expect.stringContaining("/search/users?q=u&per_page=5"),
337
+ expect.anything(),
338
+ );
339
+ });
340
+ });
341
+ });