@spaceflow/core 0.8.0 → 0.10.0
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.
- package/README.md +75 -56
- package/dist/index.js +5589 -970
- package/dist/index.js.map +1 -1
- package/package.json +9 -3
- package/src/cli-runtime/di/config.ts +157 -0
- package/src/cli-runtime/di/container.ts +120 -0
- package/src/cli-runtime/di/index.ts +3 -0
- package/src/cli-runtime/di/services.ts +53 -0
- package/src/cli-runtime/extension-loader.ts +74 -0
- package/src/{shared → cli-runtime}/i18n/index.ts +7 -1
- package/src/cli-runtime/i18n/init.ts +117 -0
- package/src/cli-runtime/index.ts +131 -0
- package/src/cli-runtime/internal-extensions.ts +33 -0
- package/src/commands/build/build.service.ts +323 -0
- package/src/commands/build/index.ts +49 -0
- package/src/commands/clear/clear.service.ts +159 -0
- package/src/commands/clear/index.ts +35 -0
- package/src/commands/commit/commit.config.ts +168 -0
- package/src/commands/commit/commit.service.ts +950 -0
- package/src/commands/commit/index.ts +58 -0
- package/src/commands/create/create.service.ts +318 -0
- package/src/commands/create/index.ts +42 -0
- package/src/commands/dev/index.ts +30 -0
- package/src/commands/install/index.ts +65 -0
- package/src/commands/install/install.service.ts +1539 -0
- package/src/commands/list/index.ts +33 -0
- package/src/commands/list/list.service.ts +127 -0
- package/src/commands/mcp/index.ts +37 -0
- package/src/commands/mcp/mcp.service.ts +246 -0
- package/src/commands/runx/index.ts +47 -0
- package/src/commands/runx/runx.service.ts +142 -0
- package/src/commands/runx/runx.utils.ts +83 -0
- package/src/commands/schema/index.ts +30 -0
- package/src/commands/setup/index.ts +34 -0
- package/src/commands/setup/setup.service.ts +234 -0
- package/src/commands/uninstall/index.ts +42 -0
- package/src/commands/uninstall/uninstall.service.ts +166 -0
- package/src/commands/update/index.ts +42 -0
- package/src/commands/update/update.service.ts +373 -0
- package/src/config/index.ts +1 -30
- package/src/config/spaceflow.config.ts +226 -278
- package/src/index.ts +11 -1
- package/src/locales/en/build.json +22 -0
- package/src/locales/en/clear.json +16 -0
- package/src/locales/en/commit.json +45 -0
- package/src/locales/en/create.json +27 -0
- package/src/locales/en/dev.json +5 -0
- package/src/locales/en/install.json +71 -0
- package/src/locales/en/list.json +8 -0
- package/src/locales/en/mcp.json +19 -0
- package/src/locales/en/runx.json +13 -0
- package/src/locales/en/schema.json +4 -0
- package/src/locales/en/setup.json +14 -0
- package/src/locales/en/uninstall.json +18 -0
- package/src/locales/en/update.json +28 -0
- package/src/locales/zh-cn/build.json +22 -0
- package/src/locales/zh-cn/clear.json +16 -0
- package/src/locales/zh-cn/commit.json +45 -0
- package/src/locales/zh-cn/create.json +27 -0
- package/src/locales/zh-cn/dev.json +5 -0
- package/src/locales/zh-cn/install.json +71 -0
- package/src/locales/zh-cn/list.json +8 -0
- package/src/locales/zh-cn/mcp.json +19 -0
- package/src/locales/zh-cn/runx.json +13 -0
- package/src/locales/zh-cn/schema.json +4 -0
- package/src/locales/zh-cn/setup.json +14 -0
- package/src/locales/zh-cn/uninstall.json +18 -0
- package/src/locales/zh-cn/update.json +28 -0
- package/src/shared/editor-config/index.ts +2 -21
- package/src/shared/llm-proxy/adapters/openai.adapter.ts +3 -1
- package/src/shared/package-manager/index.ts +5 -76
- package/src/shared/source-utils/index.ts +12 -130
- package/src/shared/spaceflow-dir/index.ts +13 -135
- package/src/shared/verbose/index.ts +10 -87
- package/dist/524.js +0 -9
- package/src/config/ci.config.ts +0 -29
- package/src/config/config-loader.ts +0 -100
- package/src/config/config-reader.service.ts +0 -128
- package/src/config/config-reader.ts +0 -75
- package/src/config/feishu.config.ts +0 -35
- package/src/config/git-provider.config.ts +0 -29
- package/src/config/llm.config.ts +0 -110
- package/src/config/load-env.ts +0 -15
- package/src/config/storage.config.ts +0 -33
- /package/src/{shared → cli-runtime}/i18n/i18n.spec.ts +0 -0
- /package/src/{shared → cli-runtime}/i18n/i18n.ts +0 -0
- /package/src/{shared → cli-runtime}/i18n/locale-detect.ts +0 -0
|
@@ -0,0 +1,1539 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { readFile, writeFile, access, mkdir, symlink, unlink, readlink, stat } from "fs/promises";
|
|
3
|
+
import { join, resolve, relative } from "path";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import {
|
|
6
|
+
shouldLog,
|
|
7
|
+
type VerboseLevel,
|
|
8
|
+
t,
|
|
9
|
+
getEditorDirName,
|
|
10
|
+
type SourceType,
|
|
11
|
+
getSourceType,
|
|
12
|
+
normalizeSource,
|
|
13
|
+
extractNpmPackageName,
|
|
14
|
+
extractName,
|
|
15
|
+
buildGitPackageSpec,
|
|
16
|
+
getPackageManager,
|
|
17
|
+
detectPackageManager,
|
|
18
|
+
getSpaceflowDir,
|
|
19
|
+
ensureSpaceflowPackageJson,
|
|
20
|
+
ensureEditorGitignore,
|
|
21
|
+
SchemaGeneratorService,
|
|
22
|
+
findConfigFileWithField,
|
|
23
|
+
getSupportedEditors,
|
|
24
|
+
getDependencies,
|
|
25
|
+
updateDependency,
|
|
26
|
+
SPACEFLOW_DIR,
|
|
27
|
+
} from "@spaceflow/core";
|
|
28
|
+
|
|
29
|
+
export type { SourceType } from "@spaceflow/core";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 扩展配置项,支持字符串或对象格式
|
|
33
|
+
* 字符串: "git@xxx.git" 或 "@scope/package@version" 或 "./path"
|
|
34
|
+
* 对象: { source: "git@xxx.git", ref: "v1.0.0" }
|
|
35
|
+
*/
|
|
36
|
+
export type ExtensionConfig =
|
|
37
|
+
| string
|
|
38
|
+
| {
|
|
39
|
+
source: string;
|
|
40
|
+
ref?: string; // git: branch/tag/commit, npm: version
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface InstallOptions {
|
|
44
|
+
source: string;
|
|
45
|
+
name?: string;
|
|
46
|
+
ref?: string; // 版本号/分支/tag/commit
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface InstallContext extends InstallOptions {
|
|
50
|
+
type: SourceType;
|
|
51
|
+
depsDir: string;
|
|
52
|
+
depPath: string;
|
|
53
|
+
configPath: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* MCP 导出项配置
|
|
58
|
+
*/
|
|
59
|
+
export interface McpExportItem {
|
|
60
|
+
name: string;
|
|
61
|
+
entry: string;
|
|
62
|
+
mcp?: { command: string; args?: string[]; env?: string[] };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 插件配置类型
|
|
67
|
+
*/
|
|
68
|
+
export type PluginConfig = Record<
|
|
69
|
+
"flows" | "commands" | "extensions",
|
|
70
|
+
Array<{ name: string; entry: string }>
|
|
71
|
+
> & {
|
|
72
|
+
mcps: McpExportItem[];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export class InstallService {
|
|
76
|
+
constructor(private readonly schemaGenerator: SchemaGeneratorService) {}
|
|
77
|
+
|
|
78
|
+
getContext(options: InstallOptions): InstallContext {
|
|
79
|
+
const cwd = process.cwd();
|
|
80
|
+
const type = getSourceType(options.source);
|
|
81
|
+
const name = options.name || extractName(options.source);
|
|
82
|
+
const spaceflowDir = join(cwd, SPACEFLOW_DIR);
|
|
83
|
+
// Extension 安装到 .spaceflow/node_modules/ 中
|
|
84
|
+
// 对 npm 包使用完整包名(含 @scope/ 前缀)作为 node_modules 路径
|
|
85
|
+
const depName = type === "npm" ? extractNpmPackageName(options.source) : name;
|
|
86
|
+
const depPath = join(spaceflowDir, "node_modules", depName);
|
|
87
|
+
const configPath = findConfigFileWithField("dependencies", cwd);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
...options,
|
|
91
|
+
type,
|
|
92
|
+
depsDir: spaceflowDir,
|
|
93
|
+
depPath,
|
|
94
|
+
configPath,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 将插件关联到各个编辑器的目录
|
|
100
|
+
* pluginConfig 包含 flows/commands/extensions/mcps 四种类型
|
|
101
|
+
* - flows: CLI 子命令,不需要复制到编辑器目录
|
|
102
|
+
* - commands: 编辑器命令,复制到 .claude/commands/ 等目录
|
|
103
|
+
* - extensions: 扩展包,复制到 .claude/skills/ 等目录
|
|
104
|
+
* - mcps: MCP Server,注册到编辑器的 mcp.json 配置
|
|
105
|
+
*/
|
|
106
|
+
protected async linkPluginToEditors(options: {
|
|
107
|
+
name: string;
|
|
108
|
+
depPath: string;
|
|
109
|
+
pluginConfig: PluginConfig;
|
|
110
|
+
cwd?: string;
|
|
111
|
+
isGlobal?: boolean;
|
|
112
|
+
verbose?: VerboseLevel;
|
|
113
|
+
}): Promise<void> {
|
|
114
|
+
const { name, depPath, pluginConfig, cwd, isGlobal = false, verbose = 1 } = options;
|
|
115
|
+
const editors = getSupportedEditors(cwd);
|
|
116
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
117
|
+
const workingDir = cwd || process.cwd();
|
|
118
|
+
|
|
119
|
+
for (const editor of editors) {
|
|
120
|
+
const editorDirName = getEditorDirName(editor);
|
|
121
|
+
const editorRoot = isGlobal ? join(home, editorDirName) : join(workingDir, editorDirName);
|
|
122
|
+
|
|
123
|
+
// 处理 extensions
|
|
124
|
+
if (pluginConfig.extensions.length > 0) {
|
|
125
|
+
const editorExtensionsDir = join(editorRoot, "skills");
|
|
126
|
+
await this.ensureDir(editorExtensionsDir, verbose);
|
|
127
|
+
|
|
128
|
+
for (const ext of pluginConfig.extensions) {
|
|
129
|
+
const extPath = ext.entry === "." ? depPath : join(depPath, ext.entry);
|
|
130
|
+
const installName = ext.name || name;
|
|
131
|
+
const targetPath = join(editorExtensionsDir, installName);
|
|
132
|
+
|
|
133
|
+
await this.copyExtensionToTarget(extPath, targetPath, installName);
|
|
134
|
+
|
|
135
|
+
// 将生成的扩展加入编辑器目录的 .gitignore
|
|
136
|
+
await ensureEditorGitignore(editorRoot, "skills", installName);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 处理 commands(编辑器命令)
|
|
141
|
+
if (pluginConfig.commands.length > 0) {
|
|
142
|
+
const editorCommandsDir = join(editorRoot, "commands");
|
|
143
|
+
await this.ensureDir(editorCommandsDir);
|
|
144
|
+
|
|
145
|
+
for (const cmd of pluginConfig.commands) {
|
|
146
|
+
const commandPath = cmd.entry === "." ? depPath : join(depPath, cmd.entry);
|
|
147
|
+
const installName = cmd.name || name;
|
|
148
|
+
await this.generateCommandMd(commandPath, editorCommandsDir, installName, verbose);
|
|
149
|
+
|
|
150
|
+
// 将生成的 command 加入编辑器目录的 .gitignore
|
|
151
|
+
await ensureEditorGitignore(editorRoot, "commands", installName);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 处理 mcps(MCP Server)
|
|
156
|
+
if (pluginConfig.mcps.length > 0) {
|
|
157
|
+
for (const mcpItem of pluginConfig.mcps) {
|
|
158
|
+
const mcpPath = mcpItem.entry === "." ? depPath : join(depPath, mcpItem.entry);
|
|
159
|
+
const installName = mcpItem.name || name;
|
|
160
|
+
await this.registerMcpServer(editorRoot, installName, mcpPath, mcpItem.mcp, verbose);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// flows 类型不需要复制到编辑器目录,它们是 CLI 子命令
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 注册 MCP Server 到编辑器的 mcp.json 配置
|
|
170
|
+
*/
|
|
171
|
+
protected async registerMcpServer(
|
|
172
|
+
editorRoot: string,
|
|
173
|
+
name: string,
|
|
174
|
+
mcpPath: string,
|
|
175
|
+
mcpConfig?: { command: string; args?: string[]; env?: string[] },
|
|
176
|
+
verbose?: VerboseLevel,
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
const mcpJsonPath = join(editorRoot, "mcp.json");
|
|
179
|
+
|
|
180
|
+
// 读取现有配置或创建新配置
|
|
181
|
+
let config: { mcpServers?: Record<string, any> } = { mcpServers: {} };
|
|
182
|
+
try {
|
|
183
|
+
if (existsSync(mcpJsonPath)) {
|
|
184
|
+
const content = await readFile(mcpJsonPath, "utf-8");
|
|
185
|
+
config = JSON.parse(content);
|
|
186
|
+
if (!config.mcpServers) {
|
|
187
|
+
config.mcpServers = {};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
config = { mcpServers: {} };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 构建 MCP Server 配置
|
|
195
|
+
const command = mcpConfig?.command || "node";
|
|
196
|
+
const args = mcpConfig?.args || ["dist/index.js"];
|
|
197
|
+
|
|
198
|
+
// 将相对路径转换为绝对路径
|
|
199
|
+
const resolvedArgs = args.map((arg) => {
|
|
200
|
+
if (arg.startsWith("./") || arg.startsWith("../") || !arg.includes("/")) {
|
|
201
|
+
// 可能是相对路径,转换为绝对路径
|
|
202
|
+
if (arg.endsWith(".js") || arg.endsWith(".mjs") || arg.endsWith(".cjs")) {
|
|
203
|
+
return join(mcpPath, arg);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return arg;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const serverConfig: Record<string, any> = {
|
|
210
|
+
command,
|
|
211
|
+
args: resolvedArgs,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// 如果有环境变量需求,添加空的 env 对象供用户填写
|
|
215
|
+
if (mcpConfig?.env && mcpConfig.env.length > 0) {
|
|
216
|
+
serverConfig.env = {};
|
|
217
|
+
for (const envKey of mcpConfig.env) {
|
|
218
|
+
serverConfig.env[envKey] = t("install:envPlaceholder", { key: envKey });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
config.mcpServers![name] = serverConfig;
|
|
223
|
+
|
|
224
|
+
// 确保目录存在
|
|
225
|
+
await this.ensureDir(editorRoot);
|
|
226
|
+
|
|
227
|
+
// 写入配置
|
|
228
|
+
await writeFile(mcpJsonPath, JSON.stringify(config, null, 2), "utf-8");
|
|
229
|
+
|
|
230
|
+
if (shouldLog(verbose, 1)) {
|
|
231
|
+
console.log(t("install:registerMcp", { name }));
|
|
232
|
+
if (mcpConfig?.env && mcpConfig.env.length > 0) {
|
|
233
|
+
console.log(t("install:mcpEnvHint", { path: mcpJsonPath, vars: mcpConfig.env.join(", ") }));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async execute(context: InstallContext, verbose: VerboseLevel = 1): Promise<void> {
|
|
239
|
+
const { source, type, depPath } = context;
|
|
240
|
+
const name = context.name || extractName(source);
|
|
241
|
+
const cwd = process.cwd();
|
|
242
|
+
const isGlobal = false;
|
|
243
|
+
|
|
244
|
+
if (shouldLog(verbose, 1)) console.log(t("install:installingExtension", { source }));
|
|
245
|
+
|
|
246
|
+
// 所有类型都通过 pnpm add 安装到 .spaceflow/node_modules/
|
|
247
|
+
await this.installExtension(source, type, context.ref, isGlobal, verbose);
|
|
248
|
+
|
|
249
|
+
// 读取插件配置并关联到编辑器
|
|
250
|
+
const pluginConfig = await this.getPluginConfigFromPackageJson(depPath);
|
|
251
|
+
await this.linkPluginToEditors({
|
|
252
|
+
name,
|
|
253
|
+
depPath,
|
|
254
|
+
pluginConfig,
|
|
255
|
+
cwd,
|
|
256
|
+
isGlobal,
|
|
257
|
+
verbose,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// 安装依赖和构建(对于本地路径的 Extension)
|
|
261
|
+
if (type === "local") {
|
|
262
|
+
const sourcePath = resolve(cwd, source);
|
|
263
|
+
await this.ensureDependenciesAndBuild(sourcePath, name, verbose);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 更新配置文件
|
|
267
|
+
await this.updateConfigFile(context, verbose);
|
|
268
|
+
|
|
269
|
+
if (shouldLog(verbose, 1)) console.log(t("install:installDone", { name }));
|
|
270
|
+
|
|
271
|
+
// 自动生成 schema
|
|
272
|
+
this.generateSchema();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 安装 Extension 到 .spaceflow/node_modules/
|
|
277
|
+
* 支持 npm 包、本地路径(link:)、git 仓库(git+)
|
|
278
|
+
* @param source 源(npm 包名、本地路径、git URL)
|
|
279
|
+
* @param type 源类型
|
|
280
|
+
* @param ref 版本/分支/tag(可选)
|
|
281
|
+
* @param isGlobal 是否安装到全局 ~/.spaceflow/
|
|
282
|
+
* @param verbose 日志级别
|
|
283
|
+
*/
|
|
284
|
+
protected async installExtension(
|
|
285
|
+
source: string,
|
|
286
|
+
type: SourceType,
|
|
287
|
+
ref?: string,
|
|
288
|
+
isGlobal: boolean = false,
|
|
289
|
+
verbose: VerboseLevel = 1,
|
|
290
|
+
): Promise<void> {
|
|
291
|
+
const spaceflowDir = getSpaceflowDir(isGlobal);
|
|
292
|
+
|
|
293
|
+
// 确保 .spaceflow 目录和 package.json 存在
|
|
294
|
+
ensureSpaceflowPackageJson(spaceflowDir);
|
|
295
|
+
|
|
296
|
+
// 根据类型构建 pnpm add 的参数
|
|
297
|
+
let packageSpec: string;
|
|
298
|
+
if (type === "local") {
|
|
299
|
+
// 本地路径使用 link: 协议,相对于 .spaceflow 目录
|
|
300
|
+
const normalizedSource = normalizeSource(source);
|
|
301
|
+
// 计算相对于 .spaceflow 目录的路径
|
|
302
|
+
const relativePath = join("..", normalizedSource);
|
|
303
|
+
packageSpec = `link:${relativePath}`;
|
|
304
|
+
if (shouldLog(verbose, 1)) {
|
|
305
|
+
console.log(t("install:typeLocal"));
|
|
306
|
+
console.log(t("install:sourcePath", { path: relativePath }));
|
|
307
|
+
}
|
|
308
|
+
} else if (type === "git") {
|
|
309
|
+
// git 仓库:如果已经是 git+ 格式则直接使用,否则转换
|
|
310
|
+
packageSpec = source.startsWith("git+") ? source : buildGitPackageSpec(source, ref);
|
|
311
|
+
if (shouldLog(verbose, 1)) {
|
|
312
|
+
console.log(t("install:typeGit"));
|
|
313
|
+
console.log(t("install:sourceUrl", { url: packageSpec }));
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
// npm 包直接使用包名
|
|
317
|
+
packageSpec = source;
|
|
318
|
+
if (shouldLog(verbose, 1)) {
|
|
319
|
+
console.log(t("install:typeNpm"));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (shouldLog(verbose, 1)) {
|
|
324
|
+
console.log(t("install:targetDir", { dir: spaceflowDir }));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const pm = detectPackageManager(spaceflowDir);
|
|
328
|
+
let cmd: string;
|
|
329
|
+
if (pm === "pnpm") {
|
|
330
|
+
cmd = `pnpm add --prefix "${spaceflowDir}" "${packageSpec}"`;
|
|
331
|
+
} else {
|
|
332
|
+
cmd = `npm install --prefix "${spaceflowDir}" "${packageSpec}"`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
execSync(cmd, {
|
|
337
|
+
cwd: process.cwd(),
|
|
338
|
+
stdio: verbose ? "inherit" : "pipe",
|
|
339
|
+
});
|
|
340
|
+
} catch (error) {
|
|
341
|
+
throw new Error(t("install:extensionInstallFailed", { source }));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* 从源获取实际的包名(用于确定 node_modules 中的路径)
|
|
347
|
+
* 本地路径:读取 package.json 的 name 字段
|
|
348
|
+
* npm 包:直接使用包名
|
|
349
|
+
* git 仓库:安装后从 node_modules 查找
|
|
350
|
+
*/
|
|
351
|
+
protected async getPackageNameFromSource(
|
|
352
|
+
source: string,
|
|
353
|
+
type: SourceType,
|
|
354
|
+
spaceflowDir: string,
|
|
355
|
+
): Promise<string> {
|
|
356
|
+
if (type === "local") {
|
|
357
|
+
// 本地路径:读取 package.json 的 name 字段(先规范化)
|
|
358
|
+
const normalizedSource = normalizeSource(source);
|
|
359
|
+
const sourcePath = resolve(process.cwd(), normalizedSource);
|
|
360
|
+
const pkgJsonPath = join(sourcePath, "package.json");
|
|
361
|
+
if (existsSync(pkgJsonPath)) {
|
|
362
|
+
try {
|
|
363
|
+
const content = await readFile(pkgJsonPath, "utf-8");
|
|
364
|
+
const pkg = JSON.parse(content);
|
|
365
|
+
if (pkg.name) {
|
|
366
|
+
return pkg.name;
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
// 解析失败,使用目录名
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// 回退到目录名
|
|
373
|
+
return extractName(source);
|
|
374
|
+
} else if (type === "npm") {
|
|
375
|
+
// npm 包:直接使用包名(去除版本号)
|
|
376
|
+
return extractNpmPackageName(source);
|
|
377
|
+
} else {
|
|
378
|
+
// git 仓库:pnpm 会将 git URL 安装为 xxx.git 格式
|
|
379
|
+
// 例如: git+ssh://git@host/org/repo.git -> repo.git
|
|
380
|
+
const baseName = extractName(source);
|
|
381
|
+
// pnpm 安装 git 仓库时会保留 .git 后缀
|
|
382
|
+
const gitName = baseName.endsWith(".git") ? baseName : `${baseName}.git`;
|
|
383
|
+
|
|
384
|
+
// 尝试在 node_modules 中查找
|
|
385
|
+
const nodeModulesPath = join(spaceflowDir, "node_modules");
|
|
386
|
+
if (existsSync(nodeModulesPath)) {
|
|
387
|
+
// 优先检查 xxx.git 格式
|
|
388
|
+
if (existsSync(join(nodeModulesPath, gitName))) {
|
|
389
|
+
return gitName;
|
|
390
|
+
}
|
|
391
|
+
// 回退检查不带 .git 的格式
|
|
392
|
+
if (existsSync(join(nodeModulesPath, baseName))) {
|
|
393
|
+
return baseName;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return gitName;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
protected async ensureDir(dirPath: string, verbose: VerboseLevel = 1): Promise<void> {
|
|
401
|
+
try {
|
|
402
|
+
await access(dirPath);
|
|
403
|
+
} catch {
|
|
404
|
+
if (shouldLog(verbose, 1)) console.log(t("install:creatingDir", { dir: dirPath }));
|
|
405
|
+
await mkdir(dirPath, { recursive: true });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* 克隆 git 仓库并移除 .git 目录
|
|
411
|
+
*/
|
|
412
|
+
protected async cloneAndRemoveGit(
|
|
413
|
+
repoUrl: string,
|
|
414
|
+
targetPath: string,
|
|
415
|
+
ref?: string,
|
|
416
|
+
verbose: VerboseLevel = 1,
|
|
417
|
+
): Promise<void> {
|
|
418
|
+
const { rm } = await import("fs/promises");
|
|
419
|
+
|
|
420
|
+
// 检查目标目录是否已存在
|
|
421
|
+
if (existsSync(targetPath)) {
|
|
422
|
+
if (shouldLog(verbose, 1)) console.log(t("install:dirExistsSkip"));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (shouldLog(verbose, 1)) console.log(t("install:cloningRepo"));
|
|
427
|
+
try {
|
|
428
|
+
const cloneCmd = ref
|
|
429
|
+
? `git clone --depth 1 --branch ${ref} ${repoUrl} ${targetPath}`
|
|
430
|
+
: `git clone --depth 1 ${repoUrl} ${targetPath}`;
|
|
431
|
+
execSync(cloneCmd, { stdio: verbose ? "inherit" : "pipe" });
|
|
432
|
+
} catch {
|
|
433
|
+
// 如果 --branch 失败(可能是 commit hash),先 clone 再 checkout
|
|
434
|
+
try {
|
|
435
|
+
execSync(`git clone --depth 1 ${repoUrl} ${targetPath}`, {
|
|
436
|
+
stdio: verbose ? "inherit" : "pipe",
|
|
437
|
+
});
|
|
438
|
+
if (ref) {
|
|
439
|
+
execSync(`git fetch --depth 1 origin ${ref}`, {
|
|
440
|
+
cwd: targetPath,
|
|
441
|
+
stdio: verbose ? "inherit" : "pipe",
|
|
442
|
+
});
|
|
443
|
+
execSync(`git checkout ${ref}`, { cwd: targetPath, stdio: verbose ? "inherit" : "pipe" });
|
|
444
|
+
}
|
|
445
|
+
} catch (error) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
t("install:cloneFailed", { error: error instanceof Error ? error.message : error }),
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 移除 .git 目录
|
|
453
|
+
const gitDir = join(targetPath, ".git");
|
|
454
|
+
if (existsSync(gitDir)) {
|
|
455
|
+
if (shouldLog(verbose, 1)) console.log(t("install:removingGit"));
|
|
456
|
+
await rm(gitDir, { recursive: true, force: true });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* 创建 deps 目录下的符号链接(本地路径依赖)
|
|
462
|
+
*/
|
|
463
|
+
protected async createDepsSymlink(
|
|
464
|
+
sourcePath: string,
|
|
465
|
+
depPath: string,
|
|
466
|
+
name: string,
|
|
467
|
+
): Promise<void> {
|
|
468
|
+
// 检查目标是否已存在
|
|
469
|
+
if (existsSync(depPath)) {
|
|
470
|
+
try {
|
|
471
|
+
const linkTarget = await readlink(depPath);
|
|
472
|
+
const resolvedTarget = resolve(join(depPath, ".."), linkTarget);
|
|
473
|
+
if (resolvedTarget === sourcePath) {
|
|
474
|
+
console.log(t("install:depsLinkExists"));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
await unlink(depPath);
|
|
478
|
+
} catch {
|
|
479
|
+
console.log(t("install:depsExists", { name }));
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 计算相对路径
|
|
485
|
+
const cwd = process.cwd();
|
|
486
|
+
const depsDir = join(depPath, "..");
|
|
487
|
+
const relativeSource = relative(depsDir, sourcePath);
|
|
488
|
+
|
|
489
|
+
// 显示相对于 cwd 的路径
|
|
490
|
+
const displayDepPath = relative(cwd, depPath);
|
|
491
|
+
const displaySourcePath = relative(cwd, sourcePath);
|
|
492
|
+
console.log(t("install:createDepsLink", { dep: displayDepPath, source: displaySourcePath }));
|
|
493
|
+
await symlink(relativeSource, depPath);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* 将扩展链接到 .claude/skills 目录
|
|
498
|
+
*/
|
|
499
|
+
protected async linkExtensionToTarget(
|
|
500
|
+
sourcePath: string,
|
|
501
|
+
targetPath: string,
|
|
502
|
+
name: string,
|
|
503
|
+
): Promise<void> {
|
|
504
|
+
const { rm } = await import("fs/promises");
|
|
505
|
+
|
|
506
|
+
// 检查目标是否已存在
|
|
507
|
+
if (existsSync(targetPath)) {
|
|
508
|
+
try {
|
|
509
|
+
const linkTarget = await readlink(targetPath);
|
|
510
|
+
const resolvedTarget = resolve(join(targetPath, ".."), linkTarget);
|
|
511
|
+
if (resolvedTarget === sourcePath) {
|
|
512
|
+
console.log(t("install:extensionLinkExists", { name }));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
// 链接指向不同目标,删除后重建
|
|
516
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
517
|
+
} catch {
|
|
518
|
+
// 不是符号链接,删除后重建
|
|
519
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// 计算相对路径
|
|
524
|
+
const targetDir = join(targetPath, "..");
|
|
525
|
+
const relativeSource = relative(targetDir, sourcePath);
|
|
526
|
+
|
|
527
|
+
console.log(t("install:createExtensionLink", { name, target: relativeSource }));
|
|
528
|
+
await symlink(relativeSource, targetPath);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* 将扩展复制到 .claude/skills 目录
|
|
533
|
+
*/
|
|
534
|
+
protected async copyExtensionToTarget(
|
|
535
|
+
sourcePath: string,
|
|
536
|
+
targetPath: string,
|
|
537
|
+
name: string,
|
|
538
|
+
): Promise<void> {
|
|
539
|
+
const { rm, cp } = await import("fs/promises");
|
|
540
|
+
|
|
541
|
+
// 如果目标已存在,先删除
|
|
542
|
+
if (existsSync(targetPath)) {
|
|
543
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
console.log(t("install:copyExtension", { name }));
|
|
547
|
+
await cp(sourcePath, targetPath, { recursive: true });
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* 解析扩展配置,支持字符串和对象格式
|
|
552
|
+
*/
|
|
553
|
+
parseExtensionConfig(config: ExtensionConfig): { source: string; ref?: string } {
|
|
554
|
+
if (typeof config === "string") {
|
|
555
|
+
return { source: config };
|
|
556
|
+
}
|
|
557
|
+
return { source: config.source, ref: config.ref };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* 获取安装根目录
|
|
562
|
+
* @param isGlobal 是否全局安装
|
|
563
|
+
*/
|
|
564
|
+
protected getInstallRoot(isGlobal: boolean): string {
|
|
565
|
+
if (isGlobal) {
|
|
566
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
567
|
+
return join(home, ".spaceflow");
|
|
568
|
+
}
|
|
569
|
+
return join(process.cwd(), ".spaceflow");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* 全局安装单个依赖
|
|
574
|
+
* 安装到 ~/.spaceflow/node_modules/
|
|
575
|
+
*/
|
|
576
|
+
async installGlobal(options: InstallOptions, verbose: VerboseLevel = 1): Promise<void> {
|
|
577
|
+
const { source, name, ref } = options;
|
|
578
|
+
const spaceflowDir = getSpaceflowDir(true);
|
|
579
|
+
const depName = name || extractName(source);
|
|
580
|
+
|
|
581
|
+
if (shouldLog(verbose, 1))
|
|
582
|
+
console.log(t("install:globalInstalling", { name: depName, dir: spaceflowDir }));
|
|
583
|
+
|
|
584
|
+
const sourceType = getSourceType(source);
|
|
585
|
+
|
|
586
|
+
// 通过 pnpm add 安装到 ~/.spaceflow/node_modules/
|
|
587
|
+
await this.installExtension(source, sourceType, ref, true, verbose);
|
|
588
|
+
|
|
589
|
+
// Extension 安装后的路径
|
|
590
|
+
// 对 npm 包使用完整包名(含 @scope/ 前缀)作为 node_modules 路径
|
|
591
|
+
const depModuleName = sourceType === "npm" ? name || extractNpmPackageName(source) : depName;
|
|
592
|
+
const depPath = join(spaceflowDir, "node_modules", depModuleName);
|
|
593
|
+
|
|
594
|
+
// 读取插件配置
|
|
595
|
+
const pluginConfig = await this.getPluginConfigFromPackageJson(depPath);
|
|
596
|
+
|
|
597
|
+
const activeTypes = Object.entries(pluginConfig)
|
|
598
|
+
.filter(([, items]) => items.length > 0)
|
|
599
|
+
.map(([type]) => type);
|
|
600
|
+
if (activeTypes.length > 0 && shouldLog(verbose, 1)) {
|
|
601
|
+
console.log(t("install:pluginTypes", { types: activeTypes.join(", ") }));
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// 将插件关联到各个编辑器的目录
|
|
605
|
+
await this.linkPluginToEditors({
|
|
606
|
+
name: depName,
|
|
607
|
+
depPath,
|
|
608
|
+
pluginConfig,
|
|
609
|
+
cwd: process.cwd(),
|
|
610
|
+
isGlobal: true,
|
|
611
|
+
verbose,
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// 对于本地路径的 Extension,需要安装依赖和构建
|
|
615
|
+
if (sourceType === "local") {
|
|
616
|
+
const sourcePath = resolve(process.cwd(), source);
|
|
617
|
+
await this.ensureDependenciesAndBuild(sourcePath, depName, verbose);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (shouldLog(verbose, 1)) console.log(t("install:globalInstallDone"));
|
|
621
|
+
|
|
622
|
+
// 自动生成 schema
|
|
623
|
+
this.generateSchema();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* 检查 .spaceflowrc 中声明的依赖是否有未安装的
|
|
628
|
+
*/
|
|
629
|
+
hasMissingExtensions(): boolean {
|
|
630
|
+
const cwd = process.cwd();
|
|
631
|
+
const dependencies = this.parseExtensionsFromConfig(cwd);
|
|
632
|
+
const spaceflowDir = getSpaceflowDir(false);
|
|
633
|
+
const globalDir = getSpaceflowDir(true);
|
|
634
|
+
|
|
635
|
+
for (const name of Object.keys(dependencies)) {
|
|
636
|
+
const localInstalled = existsSync(join(spaceflowDir, "node_modules", name, "package.json"));
|
|
637
|
+
const globalInstalled = existsSync(join(globalDir, "node_modules", name, "package.json"));
|
|
638
|
+
if (!localInstalled && !globalInstalled) {
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* 更新配置文件中的所有依赖
|
|
647
|
+
* 先更新 .spaceflow/package.json,然后一次性安装所有依赖
|
|
648
|
+
*/
|
|
649
|
+
async updateAllExtensions(options?: { verbose?: VerboseLevel }): Promise<void> {
|
|
650
|
+
const cwd = process.cwd();
|
|
651
|
+
const spaceflowDir = getSpaceflowDir(false);
|
|
652
|
+
const verbose = options?.verbose ?? true;
|
|
653
|
+
|
|
654
|
+
if (shouldLog(verbose, 1)) console.log(t("install:updatingAll"));
|
|
655
|
+
|
|
656
|
+
// 读取配置文件中的 dependencies
|
|
657
|
+
const dependencies = this.parseExtensionsFromConfig(cwd);
|
|
658
|
+
|
|
659
|
+
if (Object.keys(dependencies).length === 0) {
|
|
660
|
+
if (shouldLog(verbose, 1)) console.log(t("install:noDeps"));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (shouldLog(verbose, 1))
|
|
665
|
+
console.log(t("install:foundDeps", { count: Object.keys(dependencies).length }));
|
|
666
|
+
|
|
667
|
+
// 1. 先更新 .spaceflow/package.json 中的所有依赖
|
|
668
|
+
await this.updateSpaceflowPackageJson(dependencies, spaceflowDir, verbose);
|
|
669
|
+
|
|
670
|
+
// 2. 一次性安装所有依赖
|
|
671
|
+
if (shouldLog(verbose, 1)) console.log(t("install:installingDeps"));
|
|
672
|
+
const pm = detectPackageManager(spaceflowDir);
|
|
673
|
+
try {
|
|
674
|
+
execSync(`${pm} install`, { cwd: spaceflowDir, stdio: verbose ? "inherit" : "pipe" });
|
|
675
|
+
} catch {
|
|
676
|
+
console.warn(t("install:pmInstallFailed", { pm }));
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// 3. 处理每个依赖的 extensions/commands 关联
|
|
680
|
+
for (const [name, config] of Object.entries(dependencies)) {
|
|
681
|
+
const { source } = this.parseExtensionConfig(config);
|
|
682
|
+
const sourceType = getSourceType(source);
|
|
683
|
+
|
|
684
|
+
// 获取安装后的路径
|
|
685
|
+
// workspace: 和 npm 类型时,name(.spaceflowrc 的 key)就是包名
|
|
686
|
+
const packageName =
|
|
687
|
+
source.startsWith("workspace:") || sourceType === "npm"
|
|
688
|
+
? name
|
|
689
|
+
: await this.getPackageNameFromSource(source, sourceType, spaceflowDir);
|
|
690
|
+
const depPath = join(spaceflowDir, "node_modules", packageName);
|
|
691
|
+
|
|
692
|
+
if (!existsSync(depPath)) {
|
|
693
|
+
console.warn(t("install:depNotInstalled", { name }));
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// 读取插件配置并关联到编辑器
|
|
698
|
+
const pluginConfig = await this.getPluginConfigFromPackageJson(depPath);
|
|
699
|
+
const activeTypes = Object.entries(pluginConfig)
|
|
700
|
+
.filter(([, items]) => items.length > 0)
|
|
701
|
+
.map(([type]) => type);
|
|
702
|
+
|
|
703
|
+
if (activeTypes.length > 0) {
|
|
704
|
+
if (shouldLog(verbose, 1)) console.log(`\n📦 ${name}: ${activeTypes.join(", ")}`);
|
|
705
|
+
await this.linkPluginToEditors({
|
|
706
|
+
name,
|
|
707
|
+
depPath,
|
|
708
|
+
pluginConfig,
|
|
709
|
+
cwd,
|
|
710
|
+
isGlobal: false,
|
|
711
|
+
verbose,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// 对于本地路径的 Extension,需要构建(workspace: 类型不需要,已在 workspace 中构建)
|
|
716
|
+
if (sourceType === "local" && !source.startsWith("workspace:")) {
|
|
717
|
+
const normalizedSource = normalizeSource(source);
|
|
718
|
+
const sourcePath = resolve(cwd, normalizedSource);
|
|
719
|
+
await this.ensureDependenciesAndBuild(sourcePath, name, verbose);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (shouldLog(options?.verbose, 1)) {
|
|
724
|
+
console.log(t("install:allExtensionsDone"));
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// 自动生成 schema
|
|
728
|
+
this.generateSchema();
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* 更新 .spaceflow/package.json 中的依赖
|
|
733
|
+
*/
|
|
734
|
+
protected async updateSpaceflowPackageJson(
|
|
735
|
+
dependencies: Record<string, ExtensionConfig>,
|
|
736
|
+
spaceflowDir: string,
|
|
737
|
+
verbose: VerboseLevel = 1,
|
|
738
|
+
): Promise<void> {
|
|
739
|
+
// 确保目录和 package.json 存在
|
|
740
|
+
ensureSpaceflowPackageJson(spaceflowDir);
|
|
741
|
+
|
|
742
|
+
const packageJsonPath = join(spaceflowDir, "package.json");
|
|
743
|
+
const content = await readFile(packageJsonPath, "utf-8");
|
|
744
|
+
const pkg = JSON.parse(content);
|
|
745
|
+
|
|
746
|
+
if (!pkg.dependencies) {
|
|
747
|
+
pkg.dependencies = {};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
let updated = false;
|
|
751
|
+
for (const [name, config] of Object.entries(dependencies)) {
|
|
752
|
+
const { source, ref } = this.parseExtensionConfig(config);
|
|
753
|
+
const sourceType = getSourceType(source);
|
|
754
|
+
|
|
755
|
+
let packageSpec: string;
|
|
756
|
+
let packageName: string;
|
|
757
|
+
|
|
758
|
+
if (source.startsWith("workspace:")) {
|
|
759
|
+
// workspace 协议:直接透传
|
|
760
|
+
packageName = name;
|
|
761
|
+
packageSpec = source;
|
|
762
|
+
} else if (sourceType === "local") {
|
|
763
|
+
const normalizedSource = normalizeSource(source);
|
|
764
|
+
const relativePath = join("..", normalizedSource);
|
|
765
|
+
packageSpec = `link:${relativePath}`;
|
|
766
|
+
packageName = await this.getPackageNameFromSource(source, sourceType, spaceflowDir);
|
|
767
|
+
} else if (sourceType === "git") {
|
|
768
|
+
packageSpec = source.startsWith("git+") ? source : buildGitPackageSpec(source, ref);
|
|
769
|
+
packageName = await this.getPackageNameFromSource(source, sourceType, spaceflowDir);
|
|
770
|
+
} else {
|
|
771
|
+
// npm 类型:.spaceflowrc 中 key 是包名,value 是版本范围(如 "^0.37.0")
|
|
772
|
+
packageName = name;
|
|
773
|
+
packageSpec = source;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (pkg.dependencies[packageName] !== packageSpec) {
|
|
777
|
+
pkg.dependencies[packageName] = packageSpec;
|
|
778
|
+
updated = true;
|
|
779
|
+
if (shouldLog(verbose, 2)) console.log(` + ${packageName}: ${packageSpec}`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (updated) {
|
|
784
|
+
await writeFile(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
785
|
+
if (shouldLog(verbose, 1)) console.log(t("install:updatedPackageJson"));
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* 从配置文件解析扩展
|
|
791
|
+
*/
|
|
792
|
+
protected parseExtensionsFromConfig(cwd?: string): Record<string, ExtensionConfig> {
|
|
793
|
+
return getDependencies(cwd);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* 获取 git 仓库当前的 ref (commit hash 或 tag)
|
|
798
|
+
*/
|
|
799
|
+
protected async getCurrentGitRef(extPath: string): Promise<string | null> {
|
|
800
|
+
if (!existsSync(extPath)) {
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
try {
|
|
804
|
+
const result = execSync("git rev-parse HEAD", {
|
|
805
|
+
cwd: extPath,
|
|
806
|
+
encoding: "utf-8",
|
|
807
|
+
}).trim();
|
|
808
|
+
return result.substring(0, 7); // 短 hash
|
|
809
|
+
} catch {
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* 检查 ref 是否匹配(支持 tag、branch、commit)
|
|
816
|
+
*/
|
|
817
|
+
protected async isRefMatch(
|
|
818
|
+
extPath: string,
|
|
819
|
+
targetRef: string,
|
|
820
|
+
currentCommit: string | null,
|
|
821
|
+
): Promise<boolean> {
|
|
822
|
+
if (!currentCommit) return false;
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
// 检查 targetRef 是否是 tag 或 branch,获取其对应的 commit
|
|
826
|
+
const targetCommit = execSync(`git rev-parse ${targetRef}`, {
|
|
827
|
+
cwd: extPath,
|
|
828
|
+
encoding: "utf-8",
|
|
829
|
+
}).trim();
|
|
830
|
+
|
|
831
|
+
// 比较 commit hash(前7位)
|
|
832
|
+
return (
|
|
833
|
+
targetCommit.startsWith(currentCommit) ||
|
|
834
|
+
currentCommit.startsWith(targetCommit.substring(0, 7))
|
|
835
|
+
);
|
|
836
|
+
} catch {
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* 创建符号链接(使用绝对路径,用于全局安装)
|
|
843
|
+
*/
|
|
844
|
+
protected async createSymlinkAbsolute(
|
|
845
|
+
source: string,
|
|
846
|
+
target: string,
|
|
847
|
+
name: string,
|
|
848
|
+
verbose: VerboseLevel = 1,
|
|
849
|
+
): Promise<void> {
|
|
850
|
+
try {
|
|
851
|
+
const existingTarget = await readlink(target);
|
|
852
|
+
if (existingTarget === source) {
|
|
853
|
+
if (shouldLog(verbose, 1)) console.log(t("install:symlinkExists"));
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
await unlink(target);
|
|
857
|
+
} catch {
|
|
858
|
+
// 链接不存在,继续创建
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
try {
|
|
862
|
+
await symlink(source, target);
|
|
863
|
+
if (shouldLog(verbose, 1)) console.log(t("install:createSymlink", { target, source }));
|
|
864
|
+
} catch (error) {
|
|
865
|
+
throw new Error(
|
|
866
|
+
t("install:createSymlinkFailed", { error: error instanceof Error ? error.message : error }),
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* 检查 ref 是否是分支
|
|
873
|
+
*/
|
|
874
|
+
protected async isBranchRef(extPath: string, ref: string): Promise<boolean> {
|
|
875
|
+
if (!existsSync(extPath)) {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
try {
|
|
879
|
+
// 检查是否是远程分支
|
|
880
|
+
const result = execSync(`git branch -r --list "origin/${ref}"`, {
|
|
881
|
+
cwd: extPath,
|
|
882
|
+
encoding: "utf-8",
|
|
883
|
+
}).trim();
|
|
884
|
+
return result.length > 0;
|
|
885
|
+
} catch {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* 拉取最新代码
|
|
892
|
+
*/
|
|
893
|
+
protected async pullLatest(extPath: string, verbose: VerboseLevel = 1): Promise<void> {
|
|
894
|
+
try {
|
|
895
|
+
execSync("git pull", {
|
|
896
|
+
cwd: extPath,
|
|
897
|
+
stdio: verbose ? "inherit" : "pipe",
|
|
898
|
+
});
|
|
899
|
+
} catch {
|
|
900
|
+
if (shouldLog(verbose, 1)) console.warn(t("install:pullFailed"));
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* 切换到指定的 git ref
|
|
906
|
+
*/
|
|
907
|
+
protected async checkoutGitRef(
|
|
908
|
+
extPath: string,
|
|
909
|
+
ref: string,
|
|
910
|
+
verbose: VerboseLevel = 1,
|
|
911
|
+
): Promise<void> {
|
|
912
|
+
try {
|
|
913
|
+
if (shouldLog(verbose, 1)) console.log(t("install:checkoutVersion", { ref }));
|
|
914
|
+
// 先 fetch 确保有最新的 refs
|
|
915
|
+
execSync("git fetch --all --tags", {
|
|
916
|
+
cwd: extPath,
|
|
917
|
+
stdio: "pipe",
|
|
918
|
+
});
|
|
919
|
+
// checkout 到指定 ref
|
|
920
|
+
execSync(`git checkout ${ref}`, {
|
|
921
|
+
cwd: extPath,
|
|
922
|
+
stdio: verbose ? "inherit" : "pipe",
|
|
923
|
+
});
|
|
924
|
+
} catch {
|
|
925
|
+
if (shouldLog(verbose, 1)) console.warn(t("install:checkoutFailed"));
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* 检查依赖和构建(仅在需要时执行)
|
|
931
|
+
* 用于版本已匹配的情况,只检查 dist 是否存在
|
|
932
|
+
*/
|
|
933
|
+
protected async ensureDependenciesAndBuildIfNeeded(
|
|
934
|
+
extPath: string,
|
|
935
|
+
_name: string,
|
|
936
|
+
): Promise<void> {
|
|
937
|
+
const pkgJsonPath = join(extPath, "package.json");
|
|
938
|
+
|
|
939
|
+
// 检查是否有 package.json(命令型插件)
|
|
940
|
+
if (!existsSync(pkgJsonPath)) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const distIndexPath = join(extPath, "dist", "index.js");
|
|
945
|
+
const nodeModulesPath = join(extPath, "node_modules");
|
|
946
|
+
|
|
947
|
+
// 检查 node_modules 是否存在
|
|
948
|
+
if (!existsSync(nodeModulesPath)) {
|
|
949
|
+
console.log(t("install:installingDepsEllipsis"));
|
|
950
|
+
try {
|
|
951
|
+
execSync(`${this.getPackageManager()} install`, {
|
|
952
|
+
cwd: extPath,
|
|
953
|
+
stdio: "inherit",
|
|
954
|
+
});
|
|
955
|
+
} catch {
|
|
956
|
+
console.warn(t("install:depsInstallFailed"));
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
} else {
|
|
960
|
+
console.log(t("install:depsInstalled"));
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// 检查 dist 是否存在
|
|
964
|
+
if (!existsSync(distIndexPath)) {
|
|
965
|
+
console.log(t("install:buildingPlugin"));
|
|
966
|
+
try {
|
|
967
|
+
execSync("pnpm build", {
|
|
968
|
+
cwd: extPath,
|
|
969
|
+
stdio: "inherit",
|
|
970
|
+
});
|
|
971
|
+
} catch {
|
|
972
|
+
console.warn(t("install:buildFailed"));
|
|
973
|
+
}
|
|
974
|
+
} else {
|
|
975
|
+
console.log(t("install:buildExists"));
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* 克隆或更新 git 仓库(用于全局安装)
|
|
981
|
+
*/
|
|
982
|
+
protected async cloneOrUpdateRepo(
|
|
983
|
+
source: string,
|
|
984
|
+
depPath: string,
|
|
985
|
+
ref?: string,
|
|
986
|
+
verbose: VerboseLevel = 1,
|
|
987
|
+
): Promise<void> {
|
|
988
|
+
const gitDir = join(depPath, ".git");
|
|
989
|
+
|
|
990
|
+
if (existsSync(gitDir)) {
|
|
991
|
+
// 仓库已存在,更新
|
|
992
|
+
if (shouldLog(verbose, 1)) console.log(t("install:repoExists"));
|
|
993
|
+
try {
|
|
994
|
+
execSync("git fetch --all", { cwd: depPath, stdio: verbose ? "inherit" : "pipe" });
|
|
995
|
+
if (ref) {
|
|
996
|
+
const isBranch = await this.isBranchRef(depPath, ref);
|
|
997
|
+
if (isBranch) {
|
|
998
|
+
await this.checkoutGitRef(depPath, ref, verbose);
|
|
999
|
+
await this.pullLatest(depPath, verbose);
|
|
1000
|
+
} else {
|
|
1001
|
+
await this.checkoutGitRef(depPath, ref, verbose);
|
|
1002
|
+
}
|
|
1003
|
+
} else {
|
|
1004
|
+
execSync("git pull", { cwd: depPath, stdio: verbose ? "inherit" : "pipe" });
|
|
1005
|
+
}
|
|
1006
|
+
} catch {
|
|
1007
|
+
if (shouldLog(verbose, 1)) console.warn(t("install:repoUpdateFailed"));
|
|
1008
|
+
}
|
|
1009
|
+
} else {
|
|
1010
|
+
// 仓库不存在,克隆
|
|
1011
|
+
if (shouldLog(verbose, 1)) console.log(t("install:cloningRepo"));
|
|
1012
|
+
try {
|
|
1013
|
+
const cloneCmd = ref
|
|
1014
|
+
? `git clone --branch ${ref} ${source} ${depPath}`
|
|
1015
|
+
: `git clone ${source} ${depPath}`;
|
|
1016
|
+
execSync(cloneCmd, { stdio: verbose ? "inherit" : "pipe" });
|
|
1017
|
+
} catch {
|
|
1018
|
+
// 如果 --branch 失败(可能是 commit hash),先 clone 再 checkout
|
|
1019
|
+
try {
|
|
1020
|
+
execSync(`git clone ${source} ${depPath}`, { stdio: verbose ? "inherit" : "pipe" });
|
|
1021
|
+
if (ref) {
|
|
1022
|
+
await this.checkoutGitRef(depPath, ref, verbose);
|
|
1023
|
+
}
|
|
1024
|
+
} catch {
|
|
1025
|
+
if (shouldLog(verbose, 1)) console.warn(t("install:cloneRepoFailed"));
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* 从 package.json 读取插件配置
|
|
1033
|
+
* 返回 { flows: [], commands: [], extensions: [], mcps: [] } 格式的导出映射
|
|
1034
|
+
*/
|
|
1035
|
+
protected async getPluginConfigFromPackageJson(extPath: string): Promise<PluginConfig> {
|
|
1036
|
+
const createEmptyConfig = (): PluginConfig => ({
|
|
1037
|
+
flows: [],
|
|
1038
|
+
commands: [],
|
|
1039
|
+
extensions: [],
|
|
1040
|
+
mcps: [],
|
|
1041
|
+
});
|
|
1042
|
+
const createDefaultExtension = (name = ""): PluginConfig => ({
|
|
1043
|
+
flows: [],
|
|
1044
|
+
commands: [],
|
|
1045
|
+
extensions: [{ name, entry: "." }],
|
|
1046
|
+
mcps: [],
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
const addExport = (
|
|
1050
|
+
config: PluginConfig,
|
|
1051
|
+
type: string,
|
|
1052
|
+
name: string,
|
|
1053
|
+
entry: string,
|
|
1054
|
+
mcp?: { command: string; args?: string[]; env?: string[] },
|
|
1055
|
+
) => {
|
|
1056
|
+
if (type === "flow") config.flows.push({ name, entry });
|
|
1057
|
+
else if (type === "command") config.commands.push({ name, entry });
|
|
1058
|
+
else if (type === "extension") config.extensions.push({ name, entry });
|
|
1059
|
+
else if (type === "mcp") config.mcps.push({ name, entry, mcp });
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
const pkgJsonPath = join(extPath, "package.json");
|
|
1063
|
+
if (!existsSync(pkgJsonPath)) {
|
|
1064
|
+
return createDefaultExtension();
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
try {
|
|
1068
|
+
const content = await readFile(pkgJsonPath, "utf-8");
|
|
1069
|
+
const pkg = JSON.parse(content);
|
|
1070
|
+
const spaceflowConfig = pkg.spaceflow;
|
|
1071
|
+
|
|
1072
|
+
if (!spaceflowConfig) {
|
|
1073
|
+
return createDefaultExtension(pkg.name);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const config = createEmptyConfig();
|
|
1077
|
+
|
|
1078
|
+
// 完整格式:exports 对象
|
|
1079
|
+
if (spaceflowConfig.exports) {
|
|
1080
|
+
for (const [name, exp] of Object.entries(spaceflowConfig.exports)) {
|
|
1081
|
+
const {
|
|
1082
|
+
type = "flow",
|
|
1083
|
+
entry = ".",
|
|
1084
|
+
mcp,
|
|
1085
|
+
} = exp as { type?: string; entry?: string; mcp?: any };
|
|
1086
|
+
addExport(config, type, name, entry, mcp);
|
|
1087
|
+
}
|
|
1088
|
+
return config;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// 简化格式:type/entry
|
|
1092
|
+
if (spaceflowConfig.entry) {
|
|
1093
|
+
addExport(
|
|
1094
|
+
config,
|
|
1095
|
+
spaceflowConfig.type || "flow",
|
|
1096
|
+
pkg.name || "",
|
|
1097
|
+
spaceflowConfig.entry,
|
|
1098
|
+
spaceflowConfig.mcp,
|
|
1099
|
+
);
|
|
1100
|
+
return config;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return createDefaultExtension(pkg.name);
|
|
1104
|
+
} catch {
|
|
1105
|
+
return createDefaultExtension();
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* 获取包管理器
|
|
1111
|
+
* 必须同时满足:命令可用 AND lock 文件存在
|
|
1112
|
+
*/
|
|
1113
|
+
protected getPackageManager(): string {
|
|
1114
|
+
const cwd = process.cwd();
|
|
1115
|
+
|
|
1116
|
+
// pnpm: 命令可用 + pnpm-lock.yaml 存在
|
|
1117
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml"))) {
|
|
1118
|
+
try {
|
|
1119
|
+
execSync("pnpm --version", { stdio: "ignore" });
|
|
1120
|
+
return "pnpm";
|
|
1121
|
+
} catch {
|
|
1122
|
+
// pnpm 命令不可用,继续检测其他
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// yarn: 命令可用 + yarn.lock 存在
|
|
1127
|
+
if (existsSync(join(cwd, "yarn.lock"))) {
|
|
1128
|
+
try {
|
|
1129
|
+
execSync("yarn --version", { stdio: "ignore" });
|
|
1130
|
+
return "yarn";
|
|
1131
|
+
} catch {
|
|
1132
|
+
// yarn 命令不可用,继续检测其他
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// npm: 命令可用 + package-lock.json 存在
|
|
1137
|
+
if (existsSync(join(cwd, "package-lock.json"))) {
|
|
1138
|
+
try {
|
|
1139
|
+
execSync("npm --version", { stdio: "ignore" });
|
|
1140
|
+
return "npm";
|
|
1141
|
+
} catch {
|
|
1142
|
+
// npm 命令不可用
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// 默认回退到 npm
|
|
1147
|
+
return "npm";
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* 检测当前目录是否为 pnpm workspace
|
|
1152
|
+
*/
|
|
1153
|
+
protected isPnpmWorkspace(): boolean {
|
|
1154
|
+
const cwd = process.cwd();
|
|
1155
|
+
return existsSync(join(cwd, "pnpm-workspace.yaml"));
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* 生成 EXTENSION.md 文件
|
|
1160
|
+
* 解析 README.md 和 package.json,生成标准化的 EXTENSION.md
|
|
1161
|
+
*/
|
|
1162
|
+
protected async generateExtensionMd(extPath: string, name: string): Promise<void> {
|
|
1163
|
+
const extensionMdPath = join(extPath, "EXTENSION.md");
|
|
1164
|
+
|
|
1165
|
+
// 如果已存在 EXTENSION.md,跳过
|
|
1166
|
+
if (existsSync(extensionMdPath)) {
|
|
1167
|
+
console.log(t("install:extensionMdExists"));
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
let content = "";
|
|
1172
|
+
let pkgName = name;
|
|
1173
|
+
let pkgDescription = "";
|
|
1174
|
+
|
|
1175
|
+
// 读取 package.json
|
|
1176
|
+
const pkgJsonPath = join(extPath, "package.json");
|
|
1177
|
+
if (existsSync(pkgJsonPath)) {
|
|
1178
|
+
try {
|
|
1179
|
+
const pkgContent = await readFile(pkgJsonPath, "utf-8");
|
|
1180
|
+
const pkg = JSON.parse(pkgContent);
|
|
1181
|
+
pkgName = pkg.name || name;
|
|
1182
|
+
pkgDescription = pkg.description || "";
|
|
1183
|
+
} catch {
|
|
1184
|
+
// 解析失败,使用默认值
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// 读取 README.md(支持大小写)
|
|
1189
|
+
let readmeContent = "";
|
|
1190
|
+
const readmePaths = [
|
|
1191
|
+
join(extPath, "README.md"),
|
|
1192
|
+
join(extPath, "readme.md"),
|
|
1193
|
+
join(extPath, "Readme.md"),
|
|
1194
|
+
];
|
|
1195
|
+
for (const readmePath of readmePaths) {
|
|
1196
|
+
if (existsSync(readmePath)) {
|
|
1197
|
+
try {
|
|
1198
|
+
readmeContent = await readFile(readmePath, "utf-8");
|
|
1199
|
+
break;
|
|
1200
|
+
} catch {
|
|
1201
|
+
// 读取失败,继续尝试
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// 生成 EXTENSION.md 内容
|
|
1207
|
+
content = `# ${pkgName}\n\n`;
|
|
1208
|
+
|
|
1209
|
+
if (pkgDescription) {
|
|
1210
|
+
content += `${pkgDescription}\n\n`;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
if (readmeContent) {
|
|
1214
|
+
// 移除 README 中的标题(如果和 name 相同)
|
|
1215
|
+
const lines = readmeContent.split("\n");
|
|
1216
|
+
const firstLine = lines[0]?.trim();
|
|
1217
|
+
if (firstLine?.startsWith("#") && firstLine.includes(name)) {
|
|
1218
|
+
readmeContent = lines.slice(1).join("\n").trim();
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
if (readmeContent) {
|
|
1222
|
+
content += `## ${t("install:detailSection")}\n\n${readmeContent}\n`;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// 写入 EXTENSION.md
|
|
1227
|
+
try {
|
|
1228
|
+
await writeFile(extensionMdPath, content);
|
|
1229
|
+
console.log(t("install:extensionMdGenerated"));
|
|
1230
|
+
} catch {
|
|
1231
|
+
console.warn(t("install:extensionMdFailed"));
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* 生成 command 文档到 .claude/commands/xxx.md
|
|
1237
|
+
* 格式遵循 Claude Code 的 slash commands 规范
|
|
1238
|
+
*/
|
|
1239
|
+
protected async generateCommandMd(
|
|
1240
|
+
commandPath: string,
|
|
1241
|
+
commandsDir: string,
|
|
1242
|
+
name: string,
|
|
1243
|
+
verbose: VerboseLevel = 1,
|
|
1244
|
+
): Promise<void> {
|
|
1245
|
+
const commandMdPath = join(commandsDir, `${name}.md`);
|
|
1246
|
+
|
|
1247
|
+
let pkgName = name;
|
|
1248
|
+
let pkgDescription = "";
|
|
1249
|
+
|
|
1250
|
+
// 读取 package.json
|
|
1251
|
+
const cmdPkgJsonPath = join(commandPath, "package.json");
|
|
1252
|
+
if (existsSync(cmdPkgJsonPath)) {
|
|
1253
|
+
try {
|
|
1254
|
+
const pkgContent = await readFile(cmdPkgJsonPath, "utf-8");
|
|
1255
|
+
const pkg = JSON.parse(pkgContent);
|
|
1256
|
+
pkgName = pkg.name?.replace(/^@[^/]+\//, "") || name;
|
|
1257
|
+
pkgDescription = pkg.description || "";
|
|
1258
|
+
} catch {
|
|
1259
|
+
// 解析失败,使用默认值
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// 读取 README.md
|
|
1264
|
+
let readmeContent = "";
|
|
1265
|
+
const readmePaths = [
|
|
1266
|
+
join(commandPath, "README.md"),
|
|
1267
|
+
join(commandPath, "readme.md"),
|
|
1268
|
+
join(commandPath, "Readme.md"),
|
|
1269
|
+
];
|
|
1270
|
+
for (const readmePath of readmePaths) {
|
|
1271
|
+
if (existsSync(readmePath)) {
|
|
1272
|
+
try {
|
|
1273
|
+
readmeContent = await readFile(readmePath, "utf-8");
|
|
1274
|
+
break;
|
|
1275
|
+
} catch {
|
|
1276
|
+
// 读取失败,继续尝试
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// 获取命令的 help 内容
|
|
1282
|
+
let helpContent = "";
|
|
1283
|
+
try {
|
|
1284
|
+
helpContent = execSync(`pnpm spaceflow ${name} --help 2>/dev/null`, {
|
|
1285
|
+
cwd: process.cwd(),
|
|
1286
|
+
encoding: "utf-8",
|
|
1287
|
+
timeout: 10000,
|
|
1288
|
+
}).trim();
|
|
1289
|
+
// 移除 "📦 已加载 x 个插件" 这行
|
|
1290
|
+
helpContent = helpContent
|
|
1291
|
+
.split("\n")
|
|
1292
|
+
.filter((line) => !line.includes("已加载") && !line.includes("插件"))
|
|
1293
|
+
.join("\n")
|
|
1294
|
+
.trim();
|
|
1295
|
+
} catch {
|
|
1296
|
+
// 获取 help 失败,忽略
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// 生成 command md 内容(遵循 Claude Code 规范)
|
|
1300
|
+
let content = `---
|
|
1301
|
+
name: ${name}
|
|
1302
|
+
description: ${pkgDescription || t("install:commandDefault", { name })}
|
|
1303
|
+
---
|
|
1304
|
+
|
|
1305
|
+
# ${pkgName}
|
|
1306
|
+
|
|
1307
|
+
`;
|
|
1308
|
+
|
|
1309
|
+
if (pkgDescription) {
|
|
1310
|
+
content += `${pkgDescription}\n\n`;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// 添加 help 内容
|
|
1314
|
+
if (helpContent) {
|
|
1315
|
+
helpContent = helpContent.replace(/^Usage: cli/, "Usage: spaceflow");
|
|
1316
|
+
content += `## ${t("install:usageSection")}\n\n\`\`\`\n${helpContent}\n\`\`\`\n\n`;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
if (readmeContent) {
|
|
1320
|
+
// 移除 README 中的标题(如果和 name 相同)
|
|
1321
|
+
const lines = readmeContent.split("\n");
|
|
1322
|
+
const firstLine = lines[0]?.trim();
|
|
1323
|
+
if (firstLine?.startsWith("#") && firstLine.includes(name)) {
|
|
1324
|
+
readmeContent = lines.slice(1).join("\n").trim();
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
if (readmeContent) {
|
|
1328
|
+
content += `## ${t("install:detailSection")}\n\n${readmeContent}\n`;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// 写入 command md
|
|
1333
|
+
try {
|
|
1334
|
+
await writeFile(commandMdPath, content);
|
|
1335
|
+
if (shouldLog(verbose, 1)) console.log(t("install:commandMdGenerated", { name }));
|
|
1336
|
+
} catch {
|
|
1337
|
+
if (shouldLog(verbose, 1)) console.warn(t("install:commandMdFailed", { name }));
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
protected async updateConfigFile(
|
|
1342
|
+
context: InstallContext,
|
|
1343
|
+
verbose: VerboseLevel = 1,
|
|
1344
|
+
): Promise<void> {
|
|
1345
|
+
const { source, type, depPath } = context;
|
|
1346
|
+
// dependencies key 使用完整包名(npm 类型保留 @scope/ 前缀)
|
|
1347
|
+
const name =
|
|
1348
|
+
context.name || (type === "npm" ? extractNpmPackageName(source) : extractName(source));
|
|
1349
|
+
const cwd = process.cwd();
|
|
1350
|
+
|
|
1351
|
+
// 根据类型生成正确的 value(和 package.json 格式一致)
|
|
1352
|
+
let depValue: string;
|
|
1353
|
+
if (type === "npm") {
|
|
1354
|
+
// npm 类型:从已安装包的 package.json 读取实际版本号
|
|
1355
|
+
const pkgJsonPath = join(depPath, "package.json");
|
|
1356
|
+
try {
|
|
1357
|
+
const content = await readFile(pkgJsonPath, "utf-8");
|
|
1358
|
+
const pkg = JSON.parse(content);
|
|
1359
|
+
depValue = pkg.version ? `^${pkg.version}` : source;
|
|
1360
|
+
} catch {
|
|
1361
|
+
depValue = source;
|
|
1362
|
+
}
|
|
1363
|
+
} else if (type === "local") {
|
|
1364
|
+
// local 类型:写入 link: 格式
|
|
1365
|
+
depValue = source.startsWith("link:") ? source : `link:${normalizeSource(source)}`;
|
|
1366
|
+
} else {
|
|
1367
|
+
// git 类型:写入 git URL
|
|
1368
|
+
depValue = source.startsWith("git+") ? source : buildGitPackageSpec(source, context.ref);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const updated = updateDependency(name, depValue, cwd);
|
|
1372
|
+
|
|
1373
|
+
if (updated) {
|
|
1374
|
+
if (shouldLog(verbose, 1))
|
|
1375
|
+
console.log(
|
|
1376
|
+
t("install:configUpdated", { path: findConfigFileWithField("dependencies", cwd) }),
|
|
1377
|
+
);
|
|
1378
|
+
} else {
|
|
1379
|
+
if (shouldLog(verbose, 1)) console.log(t("install:configAlreadyExists"));
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* 确保依赖已安装且 dist 是最新的
|
|
1385
|
+
*/
|
|
1386
|
+
protected async ensureDependenciesAndBuild(
|
|
1387
|
+
extPath: string,
|
|
1388
|
+
name: string,
|
|
1389
|
+
verbose: VerboseLevel = 1,
|
|
1390
|
+
): Promise<void> {
|
|
1391
|
+
const pkgJsonPath = join(extPath, "package.json");
|
|
1392
|
+
|
|
1393
|
+
// 检查是否有 package.json(命令型插件)
|
|
1394
|
+
if (!existsSync(pkgJsonPath)) {
|
|
1395
|
+
// 资源型插件,无需处理
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// 检查依赖是否已安装
|
|
1400
|
+
const nodeModulesPath = join(extPath, "node_modules");
|
|
1401
|
+
const needInstall = await this.needsInstallDependencies(extPath, nodeModulesPath);
|
|
1402
|
+
|
|
1403
|
+
if (needInstall) {
|
|
1404
|
+
if (shouldLog(verbose, 1)) console.log(t("install:installingDepsEllipsis"));
|
|
1405
|
+
try {
|
|
1406
|
+
execSync(`${getPackageManager()} install`, {
|
|
1407
|
+
cwd: extPath,
|
|
1408
|
+
stdio: verbose ? "inherit" : "pipe",
|
|
1409
|
+
});
|
|
1410
|
+
} catch {
|
|
1411
|
+
if (shouldLog(verbose, 1)) console.warn(t("install:depsInstallFailed"));
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
} else {
|
|
1415
|
+
if (shouldLog(verbose, 1)) console.log(t("install:depsUpToDate"));
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// 检查是否需要构建
|
|
1419
|
+
const needBuild = await this.needsBuild(extPath);
|
|
1420
|
+
|
|
1421
|
+
if (needBuild) {
|
|
1422
|
+
if (shouldLog(verbose, 1)) console.log(t("install:buildingPlugin"));
|
|
1423
|
+
try {
|
|
1424
|
+
execSync("pnpm build", {
|
|
1425
|
+
cwd: extPath,
|
|
1426
|
+
stdio: verbose ? "inherit" : "pipe",
|
|
1427
|
+
});
|
|
1428
|
+
} catch {
|
|
1429
|
+
if (shouldLog(verbose, 1)) console.warn(t("install:buildFailed"));
|
|
1430
|
+
}
|
|
1431
|
+
} else {
|
|
1432
|
+
if (shouldLog(verbose, 1)) console.log(t("install:buildUpToDate"));
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* 检查是否需要安装依赖
|
|
1438
|
+
* 比较 package.json 和 node_modules 的修改时间
|
|
1439
|
+
*/
|
|
1440
|
+
protected async needsInstallDependencies(
|
|
1441
|
+
extPath: string,
|
|
1442
|
+
nodeModulesPath: string,
|
|
1443
|
+
): Promise<boolean> {
|
|
1444
|
+
// 如果 node_modules 不存在,需要安装
|
|
1445
|
+
if (!existsSync(nodeModulesPath)) {
|
|
1446
|
+
return true;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
try {
|
|
1450
|
+
const pkgJsonPath = join(extPath, "package.json");
|
|
1451
|
+
const lockfilePath = join(extPath, "pnpm-lock.yaml");
|
|
1452
|
+
|
|
1453
|
+
const nodeModulesStat = await stat(nodeModulesPath);
|
|
1454
|
+
const pkgJsonStat = await stat(pkgJsonPath);
|
|
1455
|
+
|
|
1456
|
+
// 如果 package.json 比 node_modules 新,需要安装
|
|
1457
|
+
if (pkgJsonStat.mtime > nodeModulesStat.mtime) {
|
|
1458
|
+
return true;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// 如果有 lockfile 且比 node_modules 新,需要安装
|
|
1462
|
+
if (existsSync(lockfilePath)) {
|
|
1463
|
+
const lockfileStat = await stat(lockfilePath);
|
|
1464
|
+
if (lockfileStat.mtime > nodeModulesStat.mtime) {
|
|
1465
|
+
return true;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
return false;
|
|
1470
|
+
} catch {
|
|
1471
|
+
return true;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* 检查是否需要构建
|
|
1477
|
+
* 比较 src 目录和 dist 目录的修改时间
|
|
1478
|
+
*/
|
|
1479
|
+
protected async needsBuild(extPath: string): Promise<boolean> {
|
|
1480
|
+
const srcPath = join(extPath, "src");
|
|
1481
|
+
const distPath = join(extPath, "dist");
|
|
1482
|
+
const distIndexPath = join(distPath, "index.js");
|
|
1483
|
+
|
|
1484
|
+
// 如果没有 src 目录,不需要构建
|
|
1485
|
+
if (!existsSync(srcPath)) {
|
|
1486
|
+
return false;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// 如果 dist/index.js 不存在,需要构建
|
|
1490
|
+
if (!existsSync(distIndexPath)) {
|
|
1491
|
+
return true;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
try {
|
|
1495
|
+
const distStat = await stat(distIndexPath);
|
|
1496
|
+
const srcMtime = await this.getLatestMtime(srcPath);
|
|
1497
|
+
|
|
1498
|
+
// 如果 src 中有文件比 dist 新,需要构建
|
|
1499
|
+
return srcMtime > distStat.mtime;
|
|
1500
|
+
} catch {
|
|
1501
|
+
return true;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* 递归获取目录中最新的修改时间
|
|
1507
|
+
*/
|
|
1508
|
+
protected async getLatestMtime(dirPath: string): Promise<Date> {
|
|
1509
|
+
const { readdir } = await import("fs/promises");
|
|
1510
|
+
let latestMtime = new Date(0);
|
|
1511
|
+
|
|
1512
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
1513
|
+
|
|
1514
|
+
for (const entry of entries) {
|
|
1515
|
+
const entryPath = join(dirPath, entry.name);
|
|
1516
|
+
|
|
1517
|
+
if (entry.isDirectory()) {
|
|
1518
|
+
const subMtime = await this.getLatestMtime(entryPath);
|
|
1519
|
+
if (subMtime > latestMtime) {
|
|
1520
|
+
latestMtime = subMtime;
|
|
1521
|
+
}
|
|
1522
|
+
} else {
|
|
1523
|
+
const entryStat = await stat(entryPath);
|
|
1524
|
+
if (entryStat.mtime > latestMtime) {
|
|
1525
|
+
latestMtime = entryStat.mtime;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
return latestMtime;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* 生成 JSON Schema
|
|
1535
|
+
*/
|
|
1536
|
+
protected generateSchema(): void {
|
|
1537
|
+
this.schemaGenerator.generate();
|
|
1538
|
+
}
|
|
1539
|
+
}
|