@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,115 @@
1
+ import { execSync } from "child_process";
2
+ import { existsSync } from "fs";
3
+ import { join } from "path";
4
+
5
+ /**
6
+ * 检测项目使用的包管理器
7
+ * 必须同时满足:命令可用 AND lock 文件存在
8
+ * @param cwd 工作目录,默认为 process.cwd()
9
+ */
10
+ export function getPackageManager(cwd?: string): string {
11
+ const workDir = cwd || process.cwd();
12
+
13
+ // pnpm: 命令可用 + pnpm-lock.yaml 存在
14
+ if (existsSync(join(workDir, "pnpm-lock.yaml"))) {
15
+ try {
16
+ execSync("pnpm --version", { stdio: "ignore" });
17
+ return "pnpm";
18
+ } catch {
19
+ // pnpm 命令不可用,继续检测其他
20
+ }
21
+ }
22
+
23
+ // yarn: 命令可用 + yarn.lock 存在
24
+ if (existsSync(join(workDir, "yarn.lock"))) {
25
+ try {
26
+ execSync("yarn --version", { stdio: "ignore" });
27
+ return "yarn";
28
+ } catch {
29
+ // yarn 命令不可用,继续检测其他
30
+ }
31
+ }
32
+
33
+ // npm: 命令可用 + package-lock.json 存在
34
+ if (existsSync(join(workDir, "package-lock.json"))) {
35
+ try {
36
+ execSync("npm --version", { stdio: "ignore" });
37
+ return "npm";
38
+ } catch {
39
+ // npm 命令不可用
40
+ }
41
+ }
42
+
43
+ // 默认回退到 npm
44
+ return "npm";
45
+ }
46
+
47
+ /**
48
+ * 检测指定目录使用的包管理器(基于 lock 文件)
49
+ * 如果没有 lock 文件,尝试检测 pnpm 是否可用
50
+ * @param dir 目标目录
51
+ */
52
+ export function detectPackageManager(dir: string): string {
53
+ if (existsSync(join(dir, "pnpm-lock.yaml"))) {
54
+ return "pnpm";
55
+ }
56
+ if (existsSync(join(dir, "yarn.lock"))) {
57
+ return "yarn";
58
+ }
59
+ if (existsSync(join(dir, "package-lock.json"))) {
60
+ return "npm";
61
+ }
62
+ // 默认使用 pnpm(如果可用)
63
+ try {
64
+ execSync("pnpm --version", { stdio: "ignore" });
65
+ return "pnpm";
66
+ } catch {
67
+ return "npm";
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 检测当前目录是否为 pnpm workspace
73
+ * @param cwd 工作目录,默认为 process.cwd()
74
+ */
75
+ export function isPnpmWorkspace(cwd?: string): boolean {
76
+ const workDir = cwd || process.cwd();
77
+ return existsSync(join(workDir, "pnpm-workspace.yaml"));
78
+ }
79
+
80
+ /**
81
+ * 将 .spaceflow 添加到根项目的 devDependencies 中
82
+ * 使用 file: 协议,兼容 npm 和 pnpm
83
+ * @param cwd 工作目录,默认为 process.cwd()
84
+ * @returns 是否成功添加(如果已存在则返回 false)
85
+ */
86
+ export function addSpaceflowToDevDependencies(cwd?: string): boolean {
87
+ const { readFileSync, writeFileSync } = require("fs");
88
+ const workDir = cwd || process.cwd();
89
+ const packageJsonPath = join(workDir, "package.json");
90
+
91
+ if (!existsSync(packageJsonPath)) {
92
+ return false;
93
+ }
94
+
95
+ try {
96
+ const content = readFileSync(packageJsonPath, "utf-8");
97
+ const pkg = JSON.parse(content);
98
+
99
+ // 检查是否已存在
100
+ if (pkg.devDependencies?.["spaceflow"]) {
101
+ return false;
102
+ }
103
+
104
+ // 添加到 devDependencies
105
+ if (!pkg.devDependencies) {
106
+ pkg.devDependencies = {};
107
+ }
108
+ pkg.devDependencies["spaceflow"] = "file:.spaceflow";
109
+
110
+ writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n");
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
@@ -0,0 +1 @@
1
+ export * from "./parallel-executor";
@@ -0,0 +1,169 @@
1
+ export interface ParallelTask<T, R> {
2
+ id: string;
3
+ data: T;
4
+ execute: (data: T) => Promise<R>;
5
+ }
6
+
7
+ export interface ParallelResult<R> {
8
+ id: string;
9
+ success: boolean;
10
+ result?: R;
11
+ error?: Error;
12
+ }
13
+
14
+ export interface ParallelExecutorOptions {
15
+ concurrency?: number;
16
+ timeout?: number;
17
+ retries?: number;
18
+ retryDelay?: number;
19
+ onProgress?: (completed: number, total: number, taskId: string) => void;
20
+ onTaskStart?: (taskId: string) => void;
21
+ onTaskComplete?: (taskId: string, success: boolean) => void;
22
+ onRetry?: (taskId: string, attempt: number, error: Error) => void;
23
+ stopOnError?: boolean;
24
+ }
25
+
26
+ export class ParallelExecutor {
27
+ private readonly concurrency: number;
28
+ private readonly timeout?: number;
29
+ private readonly retries: number;
30
+ private readonly retryDelay: number;
31
+ private readonly onProgress?: (completed: number, total: number, taskId: string) => void;
32
+ private readonly onTaskStart?: (taskId: string) => void;
33
+ private readonly onTaskComplete?: (taskId: string, success: boolean) => void;
34
+ private readonly onRetry?: (taskId: string, attempt: number, error: Error) => void;
35
+ private readonly stopOnError: boolean;
36
+
37
+ constructor(options: ParallelExecutorOptions = {}) {
38
+ this.concurrency = options.concurrency ?? 5;
39
+ this.timeout = options.timeout;
40
+ this.retries = options.retries ?? 0;
41
+ this.retryDelay = options.retryDelay ?? 1000;
42
+ this.onProgress = options.onProgress;
43
+ this.onTaskStart = options.onTaskStart;
44
+ this.onTaskComplete = options.onTaskComplete;
45
+ this.onRetry = options.onRetry;
46
+ this.stopOnError = options.stopOnError ?? false;
47
+ }
48
+
49
+ async execute<T, R>(tasks: ParallelTask<T, R>[]): Promise<ParallelResult<R>[]> {
50
+ if (tasks.length === 0) {
51
+ return [];
52
+ }
53
+
54
+ const results: ParallelResult<R>[] = [];
55
+ const total = tasks.length;
56
+ let completed = 0;
57
+ let shouldStop = false;
58
+
59
+ const executeTask = async (task: ParallelTask<T, R>): Promise<ParallelResult<R>> => {
60
+ if (shouldStop) {
61
+ return { id: task.id, success: false, error: new Error("Execution stopped") };
62
+ }
63
+
64
+ this.onTaskStart?.(task.id);
65
+
66
+ let lastError: Error | undefined;
67
+
68
+ for (let attempt = 0; attempt <= this.retries; attempt++) {
69
+ if (attempt > 0) {
70
+ this.onRetry?.(task.id, attempt, lastError!);
71
+ await this.delay(this.retryDelay);
72
+ }
73
+
74
+ try {
75
+ const result = await this.executeWithTimeout(task, task.data);
76
+ completed++;
77
+ this.onProgress?.(completed, total, task.id);
78
+ this.onTaskComplete?.(task.id, true);
79
+ return { id: task.id, success: true, result };
80
+ } catch (error) {
81
+ lastError = error instanceof Error ? error : new Error(String(error));
82
+ }
83
+ }
84
+
85
+ completed++;
86
+ this.onProgress?.(completed, total, task.id);
87
+ this.onTaskComplete?.(task.id, false);
88
+
89
+ if (this.stopOnError) {
90
+ shouldStop = true;
91
+ }
92
+
93
+ return {
94
+ id: task.id,
95
+ success: false,
96
+ error: lastError,
97
+ };
98
+ };
99
+
100
+ // 使用滑动窗口并发控制
101
+ const pending: Promise<void>[] = [];
102
+ const taskQueue = [...tasks];
103
+
104
+ while (taskQueue.length > 0 || pending.length > 0) {
105
+ // 填充到并发上限
106
+ while (pending.length < this.concurrency && taskQueue.length > 0 && !shouldStop) {
107
+ const task = taskQueue.shift()!;
108
+ const promise = executeTask(task).then((result) => {
109
+ results.push(result);
110
+ // 从 pending 中移除
111
+ const index = pending.indexOf(promise);
112
+ if (index > -1) {
113
+ pending.splice(index, 1);
114
+ }
115
+ });
116
+ pending.push(promise);
117
+ }
118
+
119
+ // 等待任意一个完成
120
+ if (pending.length > 0) {
121
+ await Promise.race(pending);
122
+ }
123
+ }
124
+
125
+ // 按原始顺序排序结果
126
+ const taskIdOrder = new Map(tasks.map((t, i) => [t.id, i]));
127
+ results.sort((a, b) => (taskIdOrder.get(a.id) ?? 0) - (taskIdOrder.get(b.id) ?? 0));
128
+
129
+ return results;
130
+ }
131
+
132
+ async map<T, R>(
133
+ items: T[],
134
+ fn: (item: T, index: number) => Promise<R>,
135
+ getId?: (item: T, index: number) => string,
136
+ ): Promise<ParallelResult<R>[]> {
137
+ const tasks: ParallelTask<{ item: T; index: number }, R>[] = items.map((item, index) => ({
138
+ id: getId ? getId(item, index) : String(index),
139
+ data: { item, index },
140
+ execute: async ({ item, index }) => fn(item, index),
141
+ }));
142
+
143
+ return this.execute(tasks);
144
+ }
145
+
146
+ private async executeWithTimeout<T, R>(task: ParallelTask<T, R>, data: T): Promise<R> {
147
+ if (!this.timeout) {
148
+ return task.execute(data);
149
+ }
150
+
151
+ return Promise.race([
152
+ task.execute(data),
153
+ new Promise<R>((_, reject) =>
154
+ setTimeout(
155
+ () => reject(new Error(`Task ${task.id} timed out after ${this.timeout}ms`)),
156
+ this.timeout,
157
+ ),
158
+ ),
159
+ ]);
160
+ }
161
+
162
+ private delay(ms: number): Promise<void> {
163
+ return new Promise((resolve) => setTimeout(resolve, ms));
164
+ }
165
+ }
166
+
167
+ export function parallel(options?: ParallelExecutorOptions): ParallelExecutor {
168
+ return new ParallelExecutor(options);
169
+ }
@@ -0,0 +1 @@
1
+ export * from "./rspack-config";
@@ -0,0 +1,157 @@
1
+ import { resolve } from "path";
2
+ import type { Configuration, RuleSetRule } from "@rspack/core";
3
+
4
+ /**
5
+ * 插件构建配置选项
6
+ */
7
+ export interface PluginBuildOptions {
8
+ /** 插件名称 */
9
+ name: string;
10
+ /** 插件根目录路径 */
11
+ path: string;
12
+ /** 入口文件路径,默认 ./src/index.ts */
13
+ entry?: string;
14
+ /** 输出目录,默认 dist */
15
+ outDir?: string;
16
+ /** 是否压缩,默认 false */
17
+ minimize?: boolean;
18
+ /** 额外的外部依赖 */
19
+ externals?: Configuration["externals"];
20
+ /** 额外的 resolve alias */
21
+ alias?: Record<string, string>;
22
+ /** 自定义 module rules */
23
+ rules?: RuleSetRule[];
24
+ }
25
+
26
+ /**
27
+ * 核心框架路径配置
28
+ */
29
+ export interface CorePathOptions {
30
+ /** core 包根目录 */
31
+ coreRoot: string;
32
+ }
33
+
34
+ /**
35
+ * 默认的外部依赖列表
36
+ */
37
+ export const DEFAULT_EXTERNALS: Configuration["externals"] = [
38
+ // Spaceflow 核心 - 运行时从 core 加载
39
+ { "@spaceflow/core": "module @spaceflow/core" },
40
+ // NestJS 相关 - 这些由 core 提供
41
+ { "@nestjs/common": "module @nestjs/common" },
42
+ { "@nestjs/config": "module @nestjs/config" },
43
+ { "@nestjs/core": "module @nestjs/core" },
44
+ { "nest-commander": "module nest-commander" },
45
+ { "reflect-metadata": "module reflect-metadata" },
46
+ // 排除所有 node_modules(除了相对路径和 src/ 别名)
47
+ /^(?!\.\/|\.\.\/|src\/)[^./]/,
48
+ ];
49
+
50
+ /**
51
+ * 默认的 TypeScript 编译规则
52
+ */
53
+ export const DEFAULT_TS_RULE: RuleSetRule = {
54
+ test: /\.ts$/,
55
+ exclude: /node_modules/,
56
+ loader: "builtin:swc-loader",
57
+ options: {
58
+ jsc: {
59
+ parser: {
60
+ syntax: "typescript",
61
+ decorators: true,
62
+ },
63
+ transform: {
64
+ legacyDecorator: true,
65
+ decoratorMetadata: true,
66
+ },
67
+ target: "es2022",
68
+ },
69
+ },
70
+ };
71
+
72
+ /**
73
+ * 创建插件构建的 rspack 配置
74
+ */
75
+ export function createPluginConfig(
76
+ options: PluginBuildOptions,
77
+ coreOptions: CorePathOptions,
78
+ ): Configuration {
79
+ const {
80
+ name,
81
+ path: pluginPath,
82
+ entry = "./src/index.ts",
83
+ outDir = "dist",
84
+ minimize = false,
85
+ externals: customExternals = [],
86
+ alias: customAlias = {},
87
+ rules: customRules = [],
88
+ } = options;
89
+
90
+ const { coreRoot } = coreOptions;
91
+
92
+ // 合并外部依赖
93
+ const mergedExternals = Array.isArray(customExternals)
94
+ ? [...(DEFAULT_EXTERNALS as any[]), ...customExternals]
95
+ : [...(DEFAULT_EXTERNALS as any[]), customExternals];
96
+
97
+ // 合并 alias
98
+ const alias = {
99
+ "@spaceflow/core": resolve(coreRoot, "src", "index.ts"),
100
+ ...customAlias,
101
+ };
102
+
103
+ // 合并 rules
104
+ const rules = [DEFAULT_TS_RULE, ...customRules];
105
+
106
+ return {
107
+ name,
108
+ context: pluginPath,
109
+ optimization: {
110
+ minimize,
111
+ },
112
+ entry: {
113
+ index: entry,
114
+ },
115
+ target: "node",
116
+ mode: process.env.NODE_ENV === "production" ? "production" : "development",
117
+ output: {
118
+ filename: "[name].js",
119
+ path: resolve(pluginPath, outDir),
120
+ library: { type: "module" },
121
+ chunkFormat: "module",
122
+ clean: true,
123
+ },
124
+ experiments: {
125
+ outputModule: true,
126
+ },
127
+ externalsType: "module-import",
128
+ externals: mergedExternals,
129
+ resolve: {
130
+ extensions: [".ts", ".js"],
131
+ extensionAlias: {
132
+ ".js": [".ts", ".js"],
133
+ },
134
+ alias,
135
+ },
136
+ module: {
137
+ rules,
138
+ },
139
+ };
140
+ }
141
+
142
+ /**
143
+ * 创建多入口插件配置
144
+ */
145
+ export function createMultiEntryPluginConfig(
146
+ options: Omit<PluginBuildOptions, "entry"> & {
147
+ entries: Record<string, string>;
148
+ },
149
+ coreOptions: CorePathOptions,
150
+ ): Configuration {
151
+ const config = createPluginConfig({ ...options, entry: "./src/index.ts" }, coreOptions);
152
+
153
+ return {
154
+ ...config,
155
+ entry: options.entries,
156
+ };
157
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Source 类型定义
3
+ */
4
+ export type SourceType = "npm" | "git" | "local";
5
+
6
+ /**
7
+ * 判断是否为 Git URL
8
+ * 支持: git@xxx.git, https://xxx.git, git+ssh://xxx, git+https://xxx
9
+ */
10
+ export function isGitUrl(source: string): boolean {
11
+ return (
12
+ source.startsWith("git@") ||
13
+ source.startsWith("git+") ||
14
+ (source.startsWith("https://") && source.endsWith(".git")) ||
15
+ source.endsWith(".git")
16
+ );
17
+ }
18
+
19
+ /**
20
+ * 判断是否为本地路径
21
+ * 支持: ./path, ../path, /path, link:./path, skills/
22
+ */
23
+ export function isLocalPath(source: string): boolean {
24
+ return (
25
+ source.startsWith("link:") ||
26
+ source.startsWith("./") ||
27
+ source.startsWith("../") ||
28
+ source.startsWith("/") ||
29
+ source.startsWith("skills/")
30
+ );
31
+ }
32
+
33
+ /**
34
+ * 获取 source 类型
35
+ */
36
+ export function getSourceType(source: string): SourceType {
37
+ if (isLocalPath(source)) return "local";
38
+ if (isGitUrl(source)) return "git";
39
+ return "npm";
40
+ }
41
+
42
+ /**
43
+ * 规范化 source(移除 link: 前缀等)
44
+ */
45
+ export function normalizeSource(source: string): string {
46
+ if (source.startsWith("link:")) {
47
+ return source.substring(5); // 移除 "link:" 前缀
48
+ }
49
+ return source;
50
+ }
51
+
52
+ /**
53
+ * 从 npm 包名中提取纯包名(去除版本号后缀)
54
+ * 例如: @spaceflow/review@0.10.0 → @spaceflow/review
55
+ * some-package@1.2.3 → some-package
56
+ */
57
+ export function extractNpmPackageName(source: string): string {
58
+ // 处理 scoped 包: @scope/name@version
59
+ if (source.startsWith("@")) {
60
+ const slashIndex = source.indexOf("/");
61
+ if (slashIndex !== -1) {
62
+ const afterSlash = source.substring(slashIndex + 1);
63
+ const atIndex = afterSlash.indexOf("@");
64
+ if (atIndex !== -1) {
65
+ return source.substring(0, slashIndex + 1 + atIndex);
66
+ }
67
+ }
68
+ return source;
69
+ }
70
+ // 处理普通包: name@version
71
+ const atIndex = source.indexOf("@");
72
+ if (atIndex !== -1) {
73
+ return source.substring(0, atIndex);
74
+ }
75
+ return source;
76
+ }
77
+
78
+ /**
79
+ * 从 source 提取名称
80
+ * npm 包: @scope/package -> package
81
+ * git URL: git@git.example.com:org/repo.git -> repo
82
+ * 本地路径: ./skills/publish -> publish
83
+ */
84
+ export function extractName(source: string): string {
85
+ if (isLocalPath(source)) {
86
+ // 本地路径:取最后一个目录名
87
+ const parts = source.replace(/\/$/, "").split("/").filter(Boolean);
88
+ return parts[parts.length - 1] || "unknown";
89
+ } else if (isGitUrl(source)) {
90
+ let path = source;
91
+ path = path.replace(/\.git$/, "");
92
+ path = path.replace(/^git@[^:]+:/, "");
93
+ path = path.replace(/^https?:\/\/[^/]+\//, "");
94
+ const parts = path.split("/").filter(Boolean);
95
+ return parts[parts.length - 1] || "unknown";
96
+ } else {
97
+ // npm 包名:先去除版本号后缀,再提取名称
98
+ // @spaceflow/review@0.10.0 → @spaceflow/review → review
99
+ const packageName = extractNpmPackageName(source);
100
+ const parts = packageName.split("/");
101
+ const lastPart = parts[parts.length - 1];
102
+ // 移除 spaceflow-plugin- 或 plugin- 前缀
103
+ return lastPart.replace(/^spaceflow-plugin-/, "").replace(/^plugin-/, "");
104
+ }
105
+ }
106
+
107
+ /**
108
+ * 构建 git 仓库的 package spec(用于 pnpm add)
109
+ * git@xxx.git -> git+ssh://git@xxx.git
110
+ * https://xxx.git -> git+https://xxx.git
111
+ */
112
+ export function buildGitPackageSpec(source: string, ref?: string): string {
113
+ let spec: string;
114
+ if (source.startsWith("git@")) {
115
+ // SSH 格式: git@host:org/repo.git -> git+ssh://git@host/org/repo.git
116
+ const sshUrl = source.replace(":", "/").replace("git@", "git+ssh://git@");
117
+ spec = sshUrl;
118
+ } else if (source.startsWith("https://")) {
119
+ spec = `git+${source}`;
120
+ } else {
121
+ spec = source;
122
+ }
123
+
124
+ // 添加 ref(分支/tag/commit)
125
+ if (ref) {
126
+ spec += `#${ref}`;
127
+ }
128
+
129
+ return spec;
130
+ }