@spaceflow/core 0.8.0 → 0.9.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/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,33 @@
|
|
|
1
|
+
import { defineExtension, type VerboseLevel } from "@spaceflow/core";
|
|
2
|
+
import { ListService } from "./list.service";
|
|
3
|
+
import type { ExtensionLoader } from "../../cli-runtime/extension-loader";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List 命令扩展
|
|
7
|
+
*/
|
|
8
|
+
export const listExtension = defineExtension({
|
|
9
|
+
name: "list",
|
|
10
|
+
version: "1.0.0",
|
|
11
|
+
description: "列出已安装的 Extension",
|
|
12
|
+
commands: [
|
|
13
|
+
{
|
|
14
|
+
name: "list",
|
|
15
|
+
description: "列出已安装的 Extension",
|
|
16
|
+
aliases: ["ls"],
|
|
17
|
+
options: [
|
|
18
|
+
{
|
|
19
|
+
flags: "-v, --verbose",
|
|
20
|
+
description: "详细输出",
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
run: async (args, options, ctx) => {
|
|
24
|
+
const verbose = (options?.verbose ? 2 : 1) as VerboseLevel;
|
|
25
|
+
const extensionLoader = ctx.getService<ExtensionLoader>("extensionLoader");
|
|
26
|
+
const listService = new ListService(extensionLoader);
|
|
27
|
+
await listService.execute(verbose);
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export * from "./list.service";
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import type { ExtensionLoader } from "../../cli-runtime/extension-loader";
|
|
4
|
+
import {
|
|
5
|
+
shouldLog,
|
|
6
|
+
type VerboseLevel,
|
|
7
|
+
getEditorDirName,
|
|
8
|
+
getSourceType,
|
|
9
|
+
normalizeSource,
|
|
10
|
+
getDependencies,
|
|
11
|
+
getSupportedEditors,
|
|
12
|
+
t,
|
|
13
|
+
} from "@spaceflow/core";
|
|
14
|
+
|
|
15
|
+
interface ExtensionListInfo {
|
|
16
|
+
name: string;
|
|
17
|
+
source: string;
|
|
18
|
+
type: "npm" | "git" | "local";
|
|
19
|
+
commands: string[];
|
|
20
|
+
installed: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ListService {
|
|
24
|
+
constructor(private readonly extensionLoader: ExtensionLoader) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 执行列表展示
|
|
28
|
+
*/
|
|
29
|
+
async execute(verbose: VerboseLevel = 1): Promise<void> {
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
|
|
32
|
+
// 读取合并后的 dependencies(支持 .spaceflowrc、.spaceflow/spaceflow.json 等所有配置源)
|
|
33
|
+
const dependencies = getDependencies(cwd);
|
|
34
|
+
|
|
35
|
+
if (Object.keys(dependencies).length === 0) {
|
|
36
|
+
if (shouldLog(verbose, 1)) {
|
|
37
|
+
console.log(t("list:noSkills"));
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log(t("list:installHint"));
|
|
40
|
+
console.log(" spaceflow install <npm-package>");
|
|
41
|
+
console.log(" spaceflow install <git-url> --name <name>");
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const editors = getSupportedEditors(cwd);
|
|
47
|
+
// 收集所有外部扩展信息
|
|
48
|
+
const extensionInfos: ExtensionListInfo[] = [];
|
|
49
|
+
for (const [name, source] of Object.entries(dependencies)) {
|
|
50
|
+
const type = getSourceType(source);
|
|
51
|
+
const installed = await this.checkInstalled(name, source, type, editors);
|
|
52
|
+
extensionInfos.push({ name, source, type, installed, commands: [] });
|
|
53
|
+
}
|
|
54
|
+
if (!shouldLog(verbose, 1)) return;
|
|
55
|
+
// 计算最大名称宽度用于对齐
|
|
56
|
+
const maxNameLen = Math.max(...extensionInfos.map((e) => e.name.length), 10);
|
|
57
|
+
const installedCount = extensionInfos.filter((e) => e.installed).length;
|
|
58
|
+
console.log(
|
|
59
|
+
t("list:installedExtensions", { installed: installedCount, total: extensionInfos.length }) +
|
|
60
|
+
"\n",
|
|
61
|
+
);
|
|
62
|
+
for (const ext of extensionInfos) {
|
|
63
|
+
const icon = ext.installed ? "\x1b[32m✔\x1b[0m" : "\x1b[33m○\x1b[0m";
|
|
64
|
+
const typeLabel =
|
|
65
|
+
ext.type === "local"
|
|
66
|
+
? "\x1b[36mlocal\x1b[0m"
|
|
67
|
+
: ext.type === "npm"
|
|
68
|
+
? "\x1b[35mnpm\x1b[0m"
|
|
69
|
+
: "\x1b[33mgit\x1b[0m";
|
|
70
|
+
const displaySource = this.getDisplaySource(ext.source, ext.type);
|
|
71
|
+
console.log(` ${icon} ${ext.name.padEnd(maxNameLen + 2)} ${typeLabel} ${displaySource}`);
|
|
72
|
+
// 显示命令列表
|
|
73
|
+
if (ext.commands.length > 0) {
|
|
74
|
+
console.log(
|
|
75
|
+
` ${"".padEnd(maxNameLen)} ${t("list:commands", { commands: ext.commands.join(", ") })}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
console.log("");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 获取用于展示的 source 字符串
|
|
84
|
+
*/
|
|
85
|
+
private getDisplaySource(source: string, type: "npm" | "git" | "local"): string {
|
|
86
|
+
if (type === "local") {
|
|
87
|
+
return normalizeSource(source);
|
|
88
|
+
}
|
|
89
|
+
if (type === "git") {
|
|
90
|
+
// 简化 git URL 展示
|
|
91
|
+
const match = source.match(/[/:](\w+\/[\w.-]+?)(?:\.git)?$/);
|
|
92
|
+
return match ? match[1] : source;
|
|
93
|
+
}
|
|
94
|
+
return source;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 检查是否已安装
|
|
99
|
+
*/
|
|
100
|
+
private async checkInstalled(
|
|
101
|
+
name: string,
|
|
102
|
+
source: string,
|
|
103
|
+
type: "npm" | "git" | "local",
|
|
104
|
+
editors: string[],
|
|
105
|
+
): Promise<boolean> {
|
|
106
|
+
const cwd = process.cwd();
|
|
107
|
+
if (type === "local") {
|
|
108
|
+
const localPath = join(cwd, normalizeSource(source));
|
|
109
|
+
return existsSync(localPath);
|
|
110
|
+
} else if (type === "npm") {
|
|
111
|
+
try {
|
|
112
|
+
require.resolve(source);
|
|
113
|
+
return true;
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
const possiblePaths = [join(cwd, "skills", name)];
|
|
119
|
+
for (const editor of editors) {
|
|
120
|
+
const editorDirName = getEditorDirName(editor);
|
|
121
|
+
possiblePaths.push(join(cwd, editorDirName, "skills", name));
|
|
122
|
+
possiblePaths.push(join(cwd, editorDirName, "commands", name));
|
|
123
|
+
}
|
|
124
|
+
return possiblePaths.some((p) => existsSync(p));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineExtension, type VerboseLevel } from "@spaceflow/core";
|
|
2
|
+
import { McpService } from "./mcp.service";
|
|
3
|
+
import type { ExtensionLoader } from "../../cli-runtime/extension-loader";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MCP 命令扩展
|
|
7
|
+
*/
|
|
8
|
+
export const mcpExtension = defineExtension({
|
|
9
|
+
name: "mcp",
|
|
10
|
+
version: "1.0.0",
|
|
11
|
+
description: "MCP 工具",
|
|
12
|
+
commands: [
|
|
13
|
+
{
|
|
14
|
+
name: "mcp",
|
|
15
|
+
description: "启动 MCP Server",
|
|
16
|
+
options: [
|
|
17
|
+
{
|
|
18
|
+
flags: "-v, --verbose",
|
|
19
|
+
description: "详细输出",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
flags: "--inspector",
|
|
23
|
+
description: "启动 MCP Inspector",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
run: async (_args, options, ctx) => {
|
|
27
|
+
const verbose = (options?.verbose ? 2 : 1) as VerboseLevel;
|
|
28
|
+
const inspector = options?.inspector as boolean | undefined;
|
|
29
|
+
const extensionLoader = ctx.getService<ExtensionLoader>("extensionLoader");
|
|
30
|
+
const mcpService = new McpService(extensionLoader);
|
|
31
|
+
await mcpService.startServer(verbose, inspector);
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export * from "./mcp.service";
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { t } from "@spaceflow/core";
|
|
2
|
+
import type { VerboseLevel } from "@spaceflow/core";
|
|
3
|
+
import { shouldLog, type McpToolMetadata } from "@spaceflow/core";
|
|
4
|
+
import type { ExtensionLoader } from "../../cli-runtime/extension-loader";
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { spawn } from "child_process";
|
|
9
|
+
|
|
10
|
+
export class McpService {
|
|
11
|
+
constructor(private readonly extensionLoader: ExtensionLoader) {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 启动 MCP Server
|
|
15
|
+
* 收集所有扩展的 MCP 工具并启动服务
|
|
16
|
+
*/
|
|
17
|
+
async startServer(verbose?: VerboseLevel, inspector?: boolean): Promise<void> {
|
|
18
|
+
// 如果启用 inspector 模式,使用 npx 启动 inspector
|
|
19
|
+
if (inspector) {
|
|
20
|
+
await this.startWithInspector();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (shouldLog(verbose, 1)) {
|
|
24
|
+
console.error(t("mcp:scanning"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 获取所有扩展(已在 exec() 阶段通过 registerExtension 注册完毕)
|
|
28
|
+
const extensions = this.extensionLoader.getExtensions();
|
|
29
|
+
const mcpServers = this.extensionLoader.getMcpServers();
|
|
30
|
+
|
|
31
|
+
if (shouldLog(verbose, 2)) {
|
|
32
|
+
console.error(t("mcp:foundExtensions", { count: extensions.length }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 收集所有扩展的 MCP 工具
|
|
36
|
+
const allTools: Array<{ tool: McpToolMetadata; handler: any; ctx: any }> = [];
|
|
37
|
+
for (const { extensionName, mcp } of mcpServers) {
|
|
38
|
+
if (shouldLog(verbose, 2)) {
|
|
39
|
+
console.error(` 扩展 ${extensionName} 提供 ${mcp.tools.length} 个 MCP 工具`);
|
|
40
|
+
}
|
|
41
|
+
for (const tool of mcp.tools) {
|
|
42
|
+
if (shouldLog(verbose, 3)) {
|
|
43
|
+
console.error(` - ${tool.name}: ${tool.description}`);
|
|
44
|
+
}
|
|
45
|
+
allTools.push({
|
|
46
|
+
tool: {
|
|
47
|
+
name: tool.name,
|
|
48
|
+
description: tool.description,
|
|
49
|
+
inputSchema: tool.inputSchema
|
|
50
|
+
? (this.zodToJsonSchema(tool.inputSchema) as any)
|
|
51
|
+
: undefined,
|
|
52
|
+
methodName: "handler",
|
|
53
|
+
},
|
|
54
|
+
handler: tool.handler,
|
|
55
|
+
ctx: this.extensionLoader["ctx"], // 获取 SpaceflowContext
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (allTools.length === 0) {
|
|
61
|
+
console.error(t("mcp:noToolsFound"));
|
|
62
|
+
console.error(t("mcp:noToolsHint"));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (shouldLog(verbose, 1)) {
|
|
67
|
+
console.error(t("mcp:toolsFound", { count: allTools.length }));
|
|
68
|
+
for (const { tool } of allTools) {
|
|
69
|
+
console.error(` - ${tool.name}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 如果 stdin 是 TTY(用户手动在终端运行),只打印信息不阻塞
|
|
74
|
+
if (process.stdin.isTTY) {
|
|
75
|
+
console.error("");
|
|
76
|
+
console.error(t("mcp:ttyHint"));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 被 MCP 客户端通过管道调用,正常启动 stdio server
|
|
81
|
+
await this.runServer(allTools, verbose);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 运行 MCP Server
|
|
86
|
+
*/
|
|
87
|
+
private async runServer(
|
|
88
|
+
allTools: Array<{ tool: McpToolMetadata; handler: any; ctx: any }>,
|
|
89
|
+
verbose?: VerboseLevel,
|
|
90
|
+
): Promise<void> {
|
|
91
|
+
const server = new McpServer({ name: "spaceflow", version: "1.0.0" });
|
|
92
|
+
|
|
93
|
+
// 注册所有工具(使用 v2 API: server.registerTool)
|
|
94
|
+
for (const { tool, handler, ctx } of allTools) {
|
|
95
|
+
// 将 JSON Schema 转换为 Zod schema
|
|
96
|
+
const schema = this.jsonSchemaToZod(tool.inputSchema);
|
|
97
|
+
server.registerTool(
|
|
98
|
+
tool.name,
|
|
99
|
+
{
|
|
100
|
+
description: tool.description,
|
|
101
|
+
inputSchema: Object.keys(schema).length > 0 ? z.object(schema) : z.object({}),
|
|
102
|
+
},
|
|
103
|
+
async (args: any) => {
|
|
104
|
+
try {
|
|
105
|
+
const result = await handler(args || {}, ctx);
|
|
106
|
+
return {
|
|
107
|
+
content: [
|
|
108
|
+
{
|
|
109
|
+
type: "text" as const,
|
|
110
|
+
text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return {
|
|
116
|
+
content: [
|
|
117
|
+
{
|
|
118
|
+
type: "text" as const,
|
|
119
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
isError: true,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 启动 stdio 传输
|
|
130
|
+
const transport = new StdioServerTransport();
|
|
131
|
+
await server.connect(transport);
|
|
132
|
+
|
|
133
|
+
if (shouldLog(verbose, 1)) {
|
|
134
|
+
console.error(t("mcp:serverStarted", { count: allTools.length }));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 保持进程运行
|
|
138
|
+
await new Promise<void>((resolve) => {
|
|
139
|
+
process.stdin.on("close", resolve);
|
|
140
|
+
process.on("SIGINT", resolve);
|
|
141
|
+
process.on("SIGTERM", resolve);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 使用 MCP Inspector 启动
|
|
147
|
+
*/
|
|
148
|
+
private async startWithInspector(): Promise<void> {
|
|
149
|
+
const args = process.argv.slice(2).filter((arg) => arg !== "--inspector");
|
|
150
|
+
const child = spawn(
|
|
151
|
+
"npx",
|
|
152
|
+
["-y", "@modelcontextprotocol/inspector", "node", process.argv[1], ...args],
|
|
153
|
+
{
|
|
154
|
+
stdio: "inherit",
|
|
155
|
+
env: { ...process.env, FORCE_COLOR: "1" },
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
child.on("exit", (code) => {
|
|
159
|
+
process.exit(code || 0);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 将 Zod schema 转换为 JSON Schema
|
|
165
|
+
*/
|
|
166
|
+
private zodToJsonSchema(zodSchema: any): Record<string, any> {
|
|
167
|
+
if (!zodSchema || typeof zodSchema.shape !== "object") {
|
|
168
|
+
return {};
|
|
169
|
+
}
|
|
170
|
+
const properties: Record<string, any> = {};
|
|
171
|
+
const required: string[] = [];
|
|
172
|
+
for (const [key, value] of Object.entries(zodSchema.shape)) {
|
|
173
|
+
const zodType = value as any;
|
|
174
|
+
let type = "string";
|
|
175
|
+
let description = zodType._def?.description;
|
|
176
|
+
// 检查是否可选
|
|
177
|
+
const isOptional = zodType._def?.typeName === "ZodOptional";
|
|
178
|
+
const innerType = isOptional ? zodType._def?.innerType : zodType;
|
|
179
|
+
// 获取类型
|
|
180
|
+
switch (innerType?._def?.typeName) {
|
|
181
|
+
case "ZodString":
|
|
182
|
+
type = "string";
|
|
183
|
+
break;
|
|
184
|
+
case "ZodNumber":
|
|
185
|
+
type = "number";
|
|
186
|
+
break;
|
|
187
|
+
case "ZodBoolean":
|
|
188
|
+
type = "boolean";
|
|
189
|
+
break;
|
|
190
|
+
case "ZodArray":
|
|
191
|
+
type = "array";
|
|
192
|
+
break;
|
|
193
|
+
default:
|
|
194
|
+
type = "string";
|
|
195
|
+
}
|
|
196
|
+
properties[key] = { type };
|
|
197
|
+
if (description) {
|
|
198
|
+
properties[key].description = description;
|
|
199
|
+
}
|
|
200
|
+
if (!isOptional) {
|
|
201
|
+
required.push(key);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { properties, required };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 将 JSON Schema 转换为 Zod schema
|
|
209
|
+
*/
|
|
210
|
+
private jsonSchemaToZod(jsonSchema?: Record<string, any>): Record<string, any> {
|
|
211
|
+
if (!jsonSchema || !jsonSchema.properties) {
|
|
212
|
+
return {};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const zodShape: Record<string, any> = {};
|
|
216
|
+
for (const [key, prop] of Object.entries(jsonSchema.properties as Record<string, any>)) {
|
|
217
|
+
const isRequired = jsonSchema.required?.includes(key);
|
|
218
|
+
let zodType: any;
|
|
219
|
+
|
|
220
|
+
switch (prop.type) {
|
|
221
|
+
case "string":
|
|
222
|
+
zodType = z.string();
|
|
223
|
+
break;
|
|
224
|
+
case "number":
|
|
225
|
+
zodType = z.number();
|
|
226
|
+
break;
|
|
227
|
+
case "boolean":
|
|
228
|
+
zodType = z.boolean();
|
|
229
|
+
break;
|
|
230
|
+
case "array":
|
|
231
|
+
zodType = z.array(z.any());
|
|
232
|
+
break;
|
|
233
|
+
default:
|
|
234
|
+
zodType = z.any();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (prop.description) {
|
|
238
|
+
zodType = zodType.describe(prop.description);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
zodShape[key] = isRequired ? zodType : zodType.optional();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return zodShape;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineExtension, SchemaGeneratorService, type VerboseLevel } from "@spaceflow/core";
|
|
2
|
+
import { RunxService } from "./runx.service";
|
|
3
|
+
import { InstallService } from "../install/install.service";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Runx 命令扩展
|
|
7
|
+
*/
|
|
8
|
+
export const runxExtension = defineExtension({
|
|
9
|
+
name: "runx",
|
|
10
|
+
version: "1.0.0",
|
|
11
|
+
description: "运行 x 命令",
|
|
12
|
+
commands: [
|
|
13
|
+
{
|
|
14
|
+
name: "runx",
|
|
15
|
+
description: "全局安装并运行扩展命令",
|
|
16
|
+
aliases: ["x"],
|
|
17
|
+
arguments: "<source> [args...]",
|
|
18
|
+
options: [
|
|
19
|
+
{
|
|
20
|
+
flags: "-n, --name <name>",
|
|
21
|
+
description: "指定安装名称",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
flags: "-v, --verbose",
|
|
25
|
+
description: "详细输出",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
run: async (args, options, _ctx) => {
|
|
29
|
+
const source = args[0];
|
|
30
|
+
const cmdArgs = args.slice(1);
|
|
31
|
+
const verbose = (options?.verbose ? 2 : 1) as VerboseLevel;
|
|
32
|
+
const schemaGenerator = new SchemaGeneratorService();
|
|
33
|
+
const installService = new InstallService(schemaGenerator);
|
|
34
|
+
const runxService = new RunxService(installService);
|
|
35
|
+
await runxService.execute({
|
|
36
|
+
source,
|
|
37
|
+
name: options?.name as string,
|
|
38
|
+
args: cmdArgs,
|
|
39
|
+
verbose,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export * from "./runx.service";
|
|
47
|
+
export * from "./runx.utils";
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { existsSync, realpathSync } from "fs";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import { InstallService } from "../install/install.service";
|
|
5
|
+
import type { VerboseLevel } from "@spaceflow/core";
|
|
6
|
+
import { extractName, getSourceType, t } from "@spaceflow/core";
|
|
7
|
+
|
|
8
|
+
export interface RunxOptions {
|
|
9
|
+
source: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
args: string[];
|
|
12
|
+
verbose?: VerboseLevel;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Runx 服务
|
|
17
|
+
* 全局安装依赖后运行命令
|
|
18
|
+
*/
|
|
19
|
+
export class RunxService {
|
|
20
|
+
constructor(private readonly installService: InstallService) {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 执行 runx:全局安装 + 运行命令
|
|
24
|
+
*/
|
|
25
|
+
async execute(options: RunxOptions): Promise<void> {
|
|
26
|
+
const { source, args } = options;
|
|
27
|
+
const verbose = options.verbose ?? true;
|
|
28
|
+
const name = options.name || extractName(source);
|
|
29
|
+
const sourceType = getSourceType(source);
|
|
30
|
+
|
|
31
|
+
// npm 包直接使用 npx 执行
|
|
32
|
+
if (sourceType === "npm") {
|
|
33
|
+
if (verbose)
|
|
34
|
+
console.log(t("runx:runningCommand", { command: `npx ${source} ${args.join(" ")}` }));
|
|
35
|
+
await this.runWithNpx(source, args);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 第一步:全局安装(静默模式)
|
|
40
|
+
await this.installService.installGlobal(
|
|
41
|
+
{
|
|
42
|
+
source,
|
|
43
|
+
name: options.name,
|
|
44
|
+
},
|
|
45
|
+
false,
|
|
46
|
+
);
|
|
47
|
+
// 第二步:运行命令
|
|
48
|
+
if (verbose) console.log(t("runx:runningCommand", { command: `${name} ${args.join(" ")}` }));
|
|
49
|
+
await this.runCommand(name, args);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 使用 npx 运行 npm 包
|
|
54
|
+
*/
|
|
55
|
+
private runWithNpx(packageName: string, args: string[]): Promise<void> {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const child = spawn("npx", [packageName, ...args], {
|
|
58
|
+
stdio: "inherit",
|
|
59
|
+
shell: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
child.on("close", (code) => {
|
|
63
|
+
if (code === 0) {
|
|
64
|
+
resolve();
|
|
65
|
+
} else {
|
|
66
|
+
reject(new Error(t("runx:npxExitCode", { package: packageName, code })));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
child.on("error", (err) => {
|
|
71
|
+
reject(err);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 运行已安装的命令
|
|
78
|
+
* TODO: 迁移到新架构后实现
|
|
79
|
+
*/
|
|
80
|
+
protected async runCommand(name: string, args: string[]): Promise<void> {
|
|
81
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
82
|
+
const depPath = join(home, ".spaceflow", "node_modules", name);
|
|
83
|
+
// 检查命令是否存在
|
|
84
|
+
if (!existsSync(depPath)) {
|
|
85
|
+
throw new Error(t("runx:commandNotInstalled", { name }));
|
|
86
|
+
}
|
|
87
|
+
// 解析符号链接获取真实路径
|
|
88
|
+
const realDepPath = realpathSync(depPath);
|
|
89
|
+
// 检查是否有 dist/index.js
|
|
90
|
+
const distPath = join(realDepPath, "dist", "index.js");
|
|
91
|
+
if (!existsSync(distPath)) {
|
|
92
|
+
throw new Error(t("runx:commandNotBuilt", { name }));
|
|
93
|
+
}
|
|
94
|
+
// 动态加载插件(使用 Function 构造器绕过 rspack 转换)
|
|
95
|
+
const importUrl = `file://${distPath}`;
|
|
96
|
+
const dynamicImport = new Function("url", "return import(url)");
|
|
97
|
+
const pluginModule = await dynamicImport(importUrl);
|
|
98
|
+
const extensionDef = pluginModule.default;
|
|
99
|
+
if (!extensionDef) {
|
|
100
|
+
throw new Error(t("runx:pluginNoExport", { name }));
|
|
101
|
+
}
|
|
102
|
+
// 新架构:extensionDef 是 ExtensionDefinition 对象
|
|
103
|
+
// 查找匹配的命令并执行
|
|
104
|
+
const commands = extensionDef.commands || [];
|
|
105
|
+
const finalArgs = this.autoCompleteCommand(
|
|
106
|
+
args,
|
|
107
|
+
commands.map((c: { name: string }) => c.name),
|
|
108
|
+
);
|
|
109
|
+
// 查找要执行的命令
|
|
110
|
+
const cmdName = finalArgs[0];
|
|
111
|
+
const cmdDef = commands.find((c: { name: string }) => c.name === cmdName);
|
|
112
|
+
if (!cmdDef) {
|
|
113
|
+
throw new Error(t("runx:commandNotFound", { name: cmdName }));
|
|
114
|
+
}
|
|
115
|
+
// TODO: 需要创建 SpaceflowContext 来执行命令
|
|
116
|
+
// 暂时抛出未实现错误
|
|
117
|
+
throw new Error(`runx 命令正在迁移到新架构,暂不可用。请直接使用 space ${cmdName} 命令。`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 自动补充命令名
|
|
122
|
+
* 如果插件只有一个命令,且用户没有指定子命令,自动在参数前补充命令名
|
|
123
|
+
*/
|
|
124
|
+
private autoCompleteCommand(args: string[], commands?: string[]): string[] {
|
|
125
|
+
// 没有命令列表或为空,直接返回
|
|
126
|
+
if (!commands || commands.length === 0) {
|
|
127
|
+
return args;
|
|
128
|
+
}
|
|
129
|
+
// 如果只有一个命令
|
|
130
|
+
if (commands.length === 1) {
|
|
131
|
+
const cmdName = commands[0];
|
|
132
|
+
// 检查用户是否已经指定了该命令
|
|
133
|
+
if (args.length === 0 || args[0] !== cmdName) {
|
|
134
|
+
// 如果第一个参数是选项(以 - 开头),说明用户没有指定子命令
|
|
135
|
+
if (args.length === 0 || args[0].startsWith("-")) {
|
|
136
|
+
return [cmdName, ...args];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return args;
|
|
141
|
+
}
|
|
142
|
+
}
|