@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,221 @@
1
+ import { parseRepoUrl } from "./parse-repo-url";
2
+
3
+ describe("parseRepoUrl", () => {
4
+ // ============ Gitea 仓库 URL ============
5
+
6
+ describe("Gitea 仓库 URL", () => {
7
+ it("应解析仓库根目录 URL(未知域名默认为 github)", () => {
8
+ const result = parseRepoUrl("https://git.bjxgj.com/xgj/review-spec");
9
+ expect(result).toEqual({
10
+ owner: "xgj",
11
+ repo: "review-spec",
12
+ path: "",
13
+ provider: "github",
14
+ serverUrl: "https://git.bjxgj.com",
15
+ });
16
+ });
17
+
18
+ it("应解析带 /src/branch/ 的目录 URL", () => {
19
+ const result = parseRepoUrl(
20
+ "https://git.bjxgj.com/xgj/review-spec/src/branch/main/references",
21
+ );
22
+ expect(result).toEqual({
23
+ owner: "xgj",
24
+ repo: "review-spec",
25
+ path: "references",
26
+ ref: "main",
27
+ provider: "gitea",
28
+ serverUrl: "https://git.bjxgj.com",
29
+ });
30
+ });
31
+
32
+ it("应解析多层子目录路径", () => {
33
+ const result = parseRepoUrl(
34
+ "https://git.example.com/org/repo/src/branch/develop/path/to/specs",
35
+ );
36
+ expect(result).toEqual({
37
+ owner: "org",
38
+ repo: "repo",
39
+ path: "path/to/specs",
40
+ ref: "develop",
41
+ provider: "gitea",
42
+ serverUrl: "https://git.example.com",
43
+ });
44
+ });
45
+
46
+ it("应解析 /src/tag/ URL", () => {
47
+ const result = parseRepoUrl("https://git.example.com/org/repo/src/tag/v1.0/docs");
48
+ expect(result).toEqual({
49
+ owner: "org",
50
+ repo: "repo",
51
+ path: "docs",
52
+ ref: "v1.0",
53
+ provider: "gitea",
54
+ serverUrl: "https://git.example.com",
55
+ });
56
+ });
57
+
58
+ it("应解析 /src/commit/ URL", () => {
59
+ const result = parseRepoUrl("https://git.example.com/org/repo/src/commit/abc123/docs");
60
+ expect(result).toEqual({
61
+ owner: "org",
62
+ repo: "repo",
63
+ path: "docs",
64
+ ref: "abc123",
65
+ provider: "gitea",
66
+ serverUrl: "https://git.example.com",
67
+ });
68
+ });
69
+
70
+ it("分支目录但无子路径时 path 应为空", () => {
71
+ const result = parseRepoUrl("https://git.example.com/org/repo/src/branch/main");
72
+ expect(result?.path).toBe("");
73
+ expect(result?.ref).toBe("main");
74
+ });
75
+ });
76
+
77
+ // ============ GitHub 仓库 URL ============
78
+
79
+ describe("GitHub 仓库 URL", () => {
80
+ it("应解析仓库根目录 URL", () => {
81
+ const result = parseRepoUrl("https://github.com/facebook/react");
82
+ expect(result).toEqual({
83
+ owner: "facebook",
84
+ repo: "react",
85
+ path: "",
86
+ provider: "github",
87
+ serverUrl: "https://github.com",
88
+ });
89
+ });
90
+
91
+ it("应解析 /tree/ 目录 URL", () => {
92
+ const result = parseRepoUrl("https://github.com/org/repo/tree/main/docs/specs");
93
+ expect(result).toEqual({
94
+ owner: "org",
95
+ repo: "repo",
96
+ path: "docs/specs",
97
+ ref: "main",
98
+ provider: "github",
99
+ serverUrl: "https://github.com",
100
+ });
101
+ });
102
+
103
+ it("分支目录但无子路径时 path 应为空", () => {
104
+ const result = parseRepoUrl("https://github.com/org/repo/tree/develop");
105
+ expect(result?.path).toBe("");
106
+ expect(result?.ref).toBe("develop");
107
+ });
108
+ });
109
+
110
+ // ============ GitLab 仓库 URL ============
111
+
112
+ describe("GitLab 仓库 URL", () => {
113
+ it("应解析仓库根目录 URL", () => {
114
+ const result = parseRepoUrl("https://gitlab.com/org/repo");
115
+ expect(result).toEqual({
116
+ owner: "org",
117
+ repo: "repo",
118
+ path: "",
119
+ provider: "gitlab",
120
+ serverUrl: "https://gitlab.com",
121
+ });
122
+ });
123
+
124
+ it("应解析 /-/tree/ 目录 URL", () => {
125
+ const result = parseRepoUrl("https://gitlab.com/org/repo/-/tree/main/docs/specs");
126
+ expect(result).toEqual({
127
+ owner: "org",
128
+ repo: "repo",
129
+ path: "docs/specs",
130
+ ref: "main",
131
+ provider: "gitlab",
132
+ serverUrl: "https://gitlab.com",
133
+ });
134
+ });
135
+
136
+ it("分支目录但无子路径时 path 应为空", () => {
137
+ const result = parseRepoUrl("https://gitlab.com/org/repo/-/tree/develop");
138
+ expect(result?.path).toBe("");
139
+ expect(result?.ref).toBe("develop");
140
+ expect(result?.provider).toBe("gitlab");
141
+ });
142
+
143
+ it("应解析 GitLab SSH 格式", () => {
144
+ const result = parseRepoUrl("git@gitlab.com:org/repo.git");
145
+ expect(result?.provider).toBe("gitlab");
146
+ expect(result?.owner).toBe("org");
147
+ expect(result?.repo).toBe("repo");
148
+ });
149
+
150
+ it("自建 GitLab 使用 /-/tree/ 格式也应识别", () => {
151
+ const result = parseRepoUrl("https://git.company.com/team/project/-/tree/main/references");
152
+ expect(result?.path).toBe("references");
153
+ expect(result?.ref).toBe("main");
154
+ });
155
+ });
156
+
157
+ // ============ SSH URL ============
158
+
159
+ describe("SSH URL", () => {
160
+ it("应解析 git@ 格式", () => {
161
+ const result = parseRepoUrl("git@git.bjxgj.com:xgj/review-spec.git");
162
+ expect(result).toEqual({
163
+ owner: "xgj",
164
+ repo: "review-spec",
165
+ path: "",
166
+ provider: "github",
167
+ serverUrl: "https://git.bjxgj.com",
168
+ });
169
+ });
170
+
171
+ it("应解析 GitHub SSH 格式", () => {
172
+ const result = parseRepoUrl("git@github.com:org/repo.git");
173
+ expect(result?.provider).toBe("github");
174
+ expect(result?.owner).toBe("org");
175
+ expect(result?.repo).toBe("repo");
176
+ });
177
+
178
+ it("应解析 git+ssh:// 格式", () => {
179
+ const result = parseRepoUrl("git+ssh://git@git.bjxgj.com/xgj/review-spec.git");
180
+ expect(result?.owner).toBe("xgj");
181
+ expect(result?.repo).toBe("review-spec");
182
+ expect(result?.provider).toBe("github");
183
+ });
184
+ });
185
+
186
+ // ============ 边界情况 ============
187
+
188
+ describe("边界情况", () => {
189
+ it("空字符串应返回 null", () => {
190
+ expect(parseRepoUrl("")).toBeNull();
191
+ });
192
+
193
+ it("纯文本应返回 null", () => {
194
+ expect(parseRepoUrl("not-a-url")).toBeNull();
195
+ });
196
+
197
+ it("只有域名无路径应返回 null", () => {
198
+ expect(parseRepoUrl("https://github.com")).toBeNull();
199
+ });
200
+
201
+ it("只有一段路径应返回 null", () => {
202
+ expect(parseRepoUrl("https://github.com/org")).toBeNull();
203
+ });
204
+
205
+ it("URL 带尾部斜杠应正常解析", () => {
206
+ const result = parseRepoUrl("https://github.com/org/repo/");
207
+ expect(result?.owner).toBe("org");
208
+ expect(result?.repo).toBe("repo");
209
+ });
210
+
211
+ it("URL 带 .git 后缀应去除", () => {
212
+ const result = parseRepoUrl("https://github.com/org/repo.git");
213
+ expect(result?.repo).toBe("repo");
214
+ });
215
+
216
+ it("本地路径应返回 null", () => {
217
+ expect(parseRepoUrl("./references")).toBeNull();
218
+ expect(parseRepoUrl("/home/user/specs")).toBeNull();
219
+ });
220
+ });
221
+ });
@@ -0,0 +1,155 @@
1
+ import type { RemoteRepoRef, GitProviderType } from "./types";
2
+ import { detectProvider } from "./detect-provider";
3
+
4
+ /** 已知的 GitHub 域名 */
5
+ const GITHUB_HOSTS = new Set(["github.com", "www.github.com"]);
6
+
7
+ /** 已知的 GitLab 域名 */
8
+ const GITLAB_HOSTS = new Set(["gitlab.com", "www.gitlab.com"]);
9
+
10
+ /**
11
+ * 解析浏览器中复制的仓库 URL 为结构化的仓库引用
12
+ *
13
+ * 支持的 URL 格式:
14
+ * - Gitea 仓库:https://git.example.com/owner/repo
15
+ * - Gitea 目录:https://git.example.com/owner/repo/src/branch/main/path/to/dir
16
+ * - Gitea 标签:https://git.example.com/owner/repo/src/tag/v1.0/path/to/dir
17
+ * - Gitea commit:https://git.example.com/owner/repo/src/commit/abc123/path/to/dir
18
+ * - GitHub 仓库:https://github.com/owner/repo
19
+ * - GitHub 目录:https://github.com/owner/repo/tree/main/path/to/dir
20
+ * - GitLab 仓库:https://gitlab.com/owner/repo
21
+ * - GitLab 目录:https://gitlab.com/owner/repo/-/tree/main/path/to/dir
22
+ * - git+ssh URL:git+ssh://git@host/owner/repo.git
23
+ * - SSH URL:git@host:owner/repo.git
24
+ *
25
+ * @returns 解析后的仓库引用,无法解析时返回 null
26
+ */
27
+ export function parseRepoUrl(url: string): RemoteRepoRef | null {
28
+ const trimmed = url.trim();
29
+ if (!trimmed) return null;
30
+ // SSH 格式: git@host:owner/repo.git
31
+ if (trimmed.startsWith("git@")) {
32
+ return parseSshUrl(trimmed);
33
+ }
34
+ // git+ssh:// 格式
35
+ if (trimmed.startsWith("git+ssh://")) {
36
+ return parseGitSshUrl(trimmed);
37
+ }
38
+ // HTTP(S) URL
39
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
40
+ return parseHttpUrl(trimmed);
41
+ }
42
+ return null;
43
+ }
44
+
45
+ /** 解析 HTTP(S) URL */
46
+ function parseHttpUrl(url: string): RemoteRepoRef | null {
47
+ let parsed: URL;
48
+ try {
49
+ parsed = new URL(url);
50
+ } catch {
51
+ return null;
52
+ }
53
+ const hostname = parsed.hostname;
54
+ const segments = parsed.pathname.split("/").filter(Boolean);
55
+ if (segments.length < 2) return null;
56
+ const owner = segments[0];
57
+ const repo = segments[1].replace(/\.git$/, "");
58
+ const serverUrl = `${parsed.protocol}//${parsed.host}`;
59
+ const isGithub = isGithubHost(hostname);
60
+ const isGitlab = isGitlabHost(hostname);
61
+ const provider: GitProviderType = isGithub
62
+ ? "github"
63
+ : isGitlab
64
+ ? "gitlab"
65
+ : detectProviderForHost(serverUrl);
66
+ // 仓库根目录(只有 owner/repo)
67
+ if (segments.length === 2) {
68
+ return { owner, repo, path: "", provider, serverUrl };
69
+ }
70
+ // GitHub: /owner/repo/tree/branch/path...
71
+ if (isGithub && segments[2] === "tree" && segments.length >= 4) {
72
+ const ref = segments[3];
73
+ const path = segments.slice(4).join("/");
74
+ return { owner, repo, path, ref, provider, serverUrl };
75
+ }
76
+ // GitLab: /owner/repo/-/tree/branch/path...
77
+ if (segments[2] === "-" && segments[3] === "tree" && segments.length >= 5) {
78
+ const ref = segments[4];
79
+ const path = segments.slice(5).join("/");
80
+ return { owner, repo, path, ref, provider: isGitlab ? "gitlab" : provider, serverUrl };
81
+ }
82
+ // Gitea: /owner/repo/src/branch/<branch>/path...
83
+ // /owner/repo/src/tag/<tag>/path...
84
+ // /owner/repo/src/commit/<sha>/path...
85
+ if (segments[2] === "src" && segments.length >= 4) {
86
+ const refType = segments[3]; // "branch", "tag", "commit"
87
+ if (
88
+ (refType === "branch" || refType === "tag" || refType === "commit") &&
89
+ segments.length >= 5
90
+ ) {
91
+ const ref = segments[4];
92
+ const path = segments.slice(5).join("/");
93
+ return { owner, repo, path, ref, provider: "gitea", serverUrl };
94
+ }
95
+ }
96
+ // 无法识别的子路径,当作仓库根目录
97
+ return { owner, repo, path: "", provider, serverUrl };
98
+ }
99
+
100
+ /** 解析 git@host:owner/repo.git 格式 */
101
+ function parseSshUrl(url: string): RemoteRepoRef | null {
102
+ const match = url.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
103
+ if (!match) return null;
104
+ const host = match[1];
105
+ const pathPart = match[2];
106
+ const segments = pathPart.split("/").filter(Boolean);
107
+ if (segments.length < 2) return null;
108
+ const owner = segments[0];
109
+ const repo = segments[1];
110
+ const serverUrl = `https://${host}`;
111
+ const provider: GitProviderType = isGithubHost(host)
112
+ ? "github"
113
+ : isGitlabHost(host)
114
+ ? "gitlab"
115
+ : detectProviderForHost(serverUrl);
116
+ return { owner, repo, path: "", provider, serverUrl };
117
+ }
118
+
119
+ /** 解析 git+ssh://git@host/owner/repo.git 格式 */
120
+ function parseGitSshUrl(url: string): RemoteRepoRef | null {
121
+ let parsed: URL;
122
+ try {
123
+ parsed = new URL(url.replace("git+ssh://", "ssh://"));
124
+ } catch {
125
+ return null;
126
+ }
127
+ const host = parsed.hostname;
128
+ const segments = parsed.pathname.split("/").filter(Boolean);
129
+ if (segments.length < 2) return null;
130
+ const owner = segments[0];
131
+ const repo = segments[1].replace(/\.git$/, "");
132
+ const serverUrl = `https://${host}`;
133
+ const provider: GitProviderType = isGithubHost(host)
134
+ ? "github"
135
+ : isGitlabHost(host)
136
+ ? "gitlab"
137
+ : detectProviderForHost(serverUrl);
138
+ return { owner, repo, path: "", provider, serverUrl };
139
+ }
140
+
141
+ /** 判断是否为 GitHub 域名 */
142
+ function isGithubHost(hostname: string): boolean {
143
+ return GITHUB_HOSTS.has(hostname) || hostname.endsWith(".github.com");
144
+ }
145
+
146
+ /** 判断是否为 GitLab 域名 */
147
+ function isGitlabHost(hostname: string): boolean {
148
+ return GITLAB_HOSTS.has(hostname) || hostname.endsWith(".gitlab.com");
149
+ }
150
+
151
+ /** 根据服务器 URL 检测 provider 类型(非 GitHub/GitLab 默认为 Gitea) */
152
+ function detectProviderForHost(serverUrl: string): GitProviderType {
153
+ const detected = detectProvider({ GIT_PROVIDER_URL: serverUrl });
154
+ return detected.provider;
155
+ }