@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,131 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { ExtensionDefinition } from "@spaceflow/core";
|
|
3
|
+
import { ServiceContainer, initializeContainer } from "./di";
|
|
4
|
+
import { ExtensionLoader } from "./extension-loader";
|
|
5
|
+
import { internalExtensions } from "./internal-extensions";
|
|
6
|
+
import { initCliI18n } from "./i18n";
|
|
7
|
+
|
|
8
|
+
export { ServiceContainer } from "./di";
|
|
9
|
+
export { ExtensionLoader } from "./extension-loader";
|
|
10
|
+
export { initCliI18n } from "./i18n";
|
|
11
|
+
export { internalExtensions } from "./internal-extensions";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Core.exec() — CLI 运行时入口
|
|
15
|
+
* 接收外部扩展列表,结合内部扩展,构建并执行 commander 程序
|
|
16
|
+
*
|
|
17
|
+
* @param extensions 外部扩展定义列表(来自 .spaceflow/bin/index.js 的静态导入)
|
|
18
|
+
*/
|
|
19
|
+
export async function exec(extensions: ExtensionDefinition[] = []): Promise<void> {
|
|
20
|
+
// 1. 初始化 i18n(如果尚未初始化,由生成的 bin/index.js 提前调用)
|
|
21
|
+
initCliI18n();
|
|
22
|
+
|
|
23
|
+
// 2. 创建并初始化服务容器
|
|
24
|
+
const container = new ServiceContainer();
|
|
25
|
+
initializeContainer(container);
|
|
26
|
+
|
|
27
|
+
// 3. 创建扩展加载器
|
|
28
|
+
const extensionLoader = new ExtensionLoader(container);
|
|
29
|
+
container.registerService("extensionLoader", extensionLoader);
|
|
30
|
+
|
|
31
|
+
// 4. 注册内部扩展
|
|
32
|
+
for (const ext of internalExtensions) {
|
|
33
|
+
extensionLoader.registerExtension(ext);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 5. 注册外部扩展(由 CLI 壳子加载并传入)
|
|
37
|
+
for (const ext of extensions) {
|
|
38
|
+
extensionLoader.registerExtension(ext);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 6. 创建 CLI 程序
|
|
42
|
+
const program = new Command();
|
|
43
|
+
program.name("spaceflow").description("Spaceflow CLI").version("1.0.0");
|
|
44
|
+
|
|
45
|
+
// 定义全局 verbose 选项(支持计数:-v, -vv, -vvv)
|
|
46
|
+
program.option(
|
|
47
|
+
"-v, --verbose",
|
|
48
|
+
"详细输出(可叠加:-v, -vv, -vvv)",
|
|
49
|
+
(_, prev: number) => prev + 1,
|
|
50
|
+
0,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// 全局选项列表
|
|
54
|
+
const globalOptions = ["-h, --help", "-V, --version", "-v, --verbose"];
|
|
55
|
+
|
|
56
|
+
// 8. 注册所有命令
|
|
57
|
+
const commands = extensionLoader.getCommands();
|
|
58
|
+
for (const cmd of commands) {
|
|
59
|
+
const command = new Command(cmd.name).description(cmd.description);
|
|
60
|
+
|
|
61
|
+
// 添加别名
|
|
62
|
+
if (cmd.aliases) {
|
|
63
|
+
for (const alias of cmd.aliases) {
|
|
64
|
+
command.alias(alias);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 添加参数
|
|
69
|
+
if (cmd.arguments) {
|
|
70
|
+
command.arguments(cmd.arguments);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 添加选项(排除已定义的全局选项)
|
|
74
|
+
if (cmd.options) {
|
|
75
|
+
for (const opt of cmd.options) {
|
|
76
|
+
if (!globalOptions.some((go) => opt.flags.startsWith(go.split(",")[0].trim()))) {
|
|
77
|
+
if (opt.isCount) {
|
|
78
|
+
command.option(
|
|
79
|
+
opt.flags as string,
|
|
80
|
+
opt.description as string,
|
|
81
|
+
(_, prev: number) => prev + 1,
|
|
82
|
+
0,
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
const defaultValue = opt.default as string | boolean | string[] | undefined;
|
|
86
|
+
command.option(opt.flags as string, opt.description as string, defaultValue);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 添加子命令
|
|
93
|
+
if (cmd.subcommands) {
|
|
94
|
+
for (const sub of cmd.subcommands) {
|
|
95
|
+
const subCmd = new Command(sub.name).description(sub.description);
|
|
96
|
+
if (sub.options) {
|
|
97
|
+
for (const opt of sub.options) {
|
|
98
|
+
if (!globalOptions.some((go) => opt.flags.startsWith(go.split(",")[0].trim()))) {
|
|
99
|
+
const defaultValue = opt.default as string | boolean | string[] | undefined;
|
|
100
|
+
subCmd.option(opt.flags as string, opt.description as string, defaultValue);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
subCmd.action(async (args, options) => {
|
|
105
|
+
await sub.run([args], options, container);
|
|
106
|
+
});
|
|
107
|
+
command.addCommand(subCmd);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 添加执行函数
|
|
112
|
+
command.action(async (...actionArgs) => {
|
|
113
|
+
const opts = actionArgs[actionArgs.length - 2] || {};
|
|
114
|
+
const positionalArgs = actionArgs.slice(0, -2);
|
|
115
|
+
// 合并全局 verbose 选项
|
|
116
|
+
const globalOpts = program.opts();
|
|
117
|
+
if (globalOpts.verbose && !opts.verbose) {
|
|
118
|
+
opts.verbose = globalOpts.verbose;
|
|
119
|
+
}
|
|
120
|
+
await cmd.run(positionalArgs, opts, container);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
program.addCommand(command);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 9. 解析命令行参数
|
|
127
|
+
await program.parseAsync(process.argv);
|
|
128
|
+
|
|
129
|
+
// 10. 清理
|
|
130
|
+
await container.destroy();
|
|
131
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { buildExtension } from "../commands/build";
|
|
2
|
+
import { clearExtension } from "../commands/clear";
|
|
3
|
+
import { commitExtension } from "../commands/commit";
|
|
4
|
+
import { createExtension } from "../commands/create";
|
|
5
|
+
import { devExtension } from "../commands/dev";
|
|
6
|
+
import { installExtension } from "../commands/install";
|
|
7
|
+
import { listExtension } from "../commands/list";
|
|
8
|
+
import { mcpExtension } from "../commands/mcp";
|
|
9
|
+
import { runxExtension } from "../commands/runx";
|
|
10
|
+
import { schemaExtension } from "../commands/schema";
|
|
11
|
+
import { setupExtension } from "../commands/setup";
|
|
12
|
+
import { uninstallExtension } from "../commands/uninstall";
|
|
13
|
+
import { updateExtension } from "../commands/update";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 内部扩展列表
|
|
17
|
+
* 从各命令目录导入 defineExtension 定义
|
|
18
|
+
*/
|
|
19
|
+
export const internalExtensions = [
|
|
20
|
+
buildExtension,
|
|
21
|
+
clearExtension,
|
|
22
|
+
commitExtension,
|
|
23
|
+
createExtension,
|
|
24
|
+
devExtension,
|
|
25
|
+
installExtension,
|
|
26
|
+
listExtension,
|
|
27
|
+
mcpExtension,
|
|
28
|
+
runxExtension,
|
|
29
|
+
schemaExtension,
|
|
30
|
+
setupExtension,
|
|
31
|
+
uninstallExtension,
|
|
32
|
+
updateExtension,
|
|
33
|
+
];
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { rspack, type Compiler, type Configuration, type Stats } from "@rspack/core";
|
|
2
|
+
import { readdir, stat } from "fs/promises";
|
|
3
|
+
import { join, dirname, resolve } from "path";
|
|
4
|
+
import { existsSync, readFileSync } from "fs";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
import { createPluginConfig, getDependencies } from "@spaceflow/core";
|
|
7
|
+
import { shouldLog, type VerboseLevel, t } from "@spaceflow/core";
|
|
8
|
+
|
|
9
|
+
export interface ExtensionInfo {
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
hasPackageJson: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface BuildResult {
|
|
16
|
+
extension: string;
|
|
17
|
+
success: boolean;
|
|
18
|
+
duration?: number;
|
|
19
|
+
errors?: string[];
|
|
20
|
+
warnings?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class BuildService {
|
|
24
|
+
private readonly projectRoot = this.findProjectRoot();
|
|
25
|
+
private readonly extensionDirs = this.discoverExtensionDirs();
|
|
26
|
+
private readonly coreRoot = this.resolveCoreRoot();
|
|
27
|
+
private watchers: Map<string, Compiler> = new Map();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 查找项目根目录(包含 spaceflow.json 或 .spaceflow/spaceflow.json 的目录)
|
|
31
|
+
*/
|
|
32
|
+
private findProjectRoot(): string {
|
|
33
|
+
let dir = process.cwd();
|
|
34
|
+
while (dir !== dirname(dir)) {
|
|
35
|
+
// 检查根目录下的 spaceflow.json
|
|
36
|
+
if (existsSync(join(dir, "spaceflow.json"))) {
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
// 检查 .spaceflow/spaceflow.json
|
|
40
|
+
if (existsSync(join(dir, ".spaceflow", "spaceflow.json"))) {
|
|
41
|
+
return dir;
|
|
42
|
+
}
|
|
43
|
+
dir = dirname(dir);
|
|
44
|
+
}
|
|
45
|
+
// 如果找不到,回退到 cwd
|
|
46
|
+
return process.cwd();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 构建插件
|
|
51
|
+
*/
|
|
52
|
+
async build(extensionName?: string, verbose: VerboseLevel = 1): Promise<BuildResult[]> {
|
|
53
|
+
const extensions = await this.getExtensionsToBuild(extensionName);
|
|
54
|
+
|
|
55
|
+
if (extensions.length === 0) {
|
|
56
|
+
if (shouldLog(verbose, 1)) console.log(t("build:noPlugins"));
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (shouldLog(verbose, 1))
|
|
61
|
+
console.log(t("build:startBuilding", { count: extensions.length }) + "\n");
|
|
62
|
+
|
|
63
|
+
const results: BuildResult[] = [];
|
|
64
|
+
for (const ext of extensions) {
|
|
65
|
+
const result = await this.buildExtension(ext, verbose);
|
|
66
|
+
results.push(result);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const successCount = results.filter((r) => r.success).length;
|
|
70
|
+
const failCount = results.filter((r) => !r.success).length;
|
|
71
|
+
|
|
72
|
+
if (shouldLog(verbose, 1))
|
|
73
|
+
console.log("\n" + t("build:buildComplete", { success: successCount, fail: failCount }));
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 监听模式构建
|
|
79
|
+
*/
|
|
80
|
+
async watch(extensionName?: string, verbose: VerboseLevel = 1): Promise<void> {
|
|
81
|
+
const extensions = await this.getExtensionsToBuild(extensionName);
|
|
82
|
+
|
|
83
|
+
if (extensions.length === 0) {
|
|
84
|
+
if (shouldLog(verbose, 1)) console.log(t("build:noPlugins"));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (shouldLog(verbose, 1))
|
|
89
|
+
console.log(t("build:startWatching", { count: extensions.length }) + "\n");
|
|
90
|
+
|
|
91
|
+
// 并行启动所有 watcher
|
|
92
|
+
await Promise.all(extensions.map((ext) => this.watchExtension(ext, verbose)));
|
|
93
|
+
|
|
94
|
+
// 保持进程运行
|
|
95
|
+
await new Promise(() => {});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 停止所有 watcher
|
|
100
|
+
*/
|
|
101
|
+
async stopWatch(verbose: VerboseLevel = 1): Promise<void> {
|
|
102
|
+
for (const [name, compiler] of this.watchers) {
|
|
103
|
+
await new Promise<void>((resolve) => {
|
|
104
|
+
compiler.close(() => {
|
|
105
|
+
if (shouldLog(verbose, 1)) console.log(t("build:stopWatching", { name }));
|
|
106
|
+
resolve();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
this.watchers.clear();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 从 spaceflow.json 的 dependencies 中发现本地 extension 所在的目录
|
|
115
|
+
* 解析所有 link: 路径,收集去重后的父目录
|
|
116
|
+
*/
|
|
117
|
+
private discoverExtensionDirs(): string[] {
|
|
118
|
+
const dependencies = getDependencies(this.projectRoot);
|
|
119
|
+
const parentDirs = new Set<string>();
|
|
120
|
+
for (const source of Object.values(dependencies)) {
|
|
121
|
+
if (!source.startsWith("link:")) continue;
|
|
122
|
+
const linkPath = source.slice(5);
|
|
123
|
+
const absolutePath = resolve(this.projectRoot, linkPath);
|
|
124
|
+
parentDirs.add(dirname(absolutePath));
|
|
125
|
+
}
|
|
126
|
+
return Array.from(parentDirs);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 动态解析 @spaceflow/core 的根目录
|
|
131
|
+
* 优先通过 require.resolve 定位,回退到 node_modules
|
|
132
|
+
*/
|
|
133
|
+
private resolveCoreRoot(): string {
|
|
134
|
+
try {
|
|
135
|
+
const req = createRequire(join(this.projectRoot, "package.json"));
|
|
136
|
+
const corePkgPath = req.resolve("@spaceflow/core/package.json");
|
|
137
|
+
return dirname(corePkgPath);
|
|
138
|
+
} catch {
|
|
139
|
+
return join(this.projectRoot, "node_modules", "@spaceflow", "core");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 获取需要构建的 Extension 列表
|
|
145
|
+
*/
|
|
146
|
+
private async getExtensionsToBuild(extensionName?: string): Promise<ExtensionInfo[]> {
|
|
147
|
+
// 如果没有指定名称,检查是否在 Extension 目录中运行
|
|
148
|
+
if (!extensionName) {
|
|
149
|
+
const current = this.detectCurrentExtension();
|
|
150
|
+
if (current) {
|
|
151
|
+
return [current];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// 从所有 extension 目录中扫描
|
|
155
|
+
const result: ExtensionInfo[] = [];
|
|
156
|
+
for (const extDir of this.extensionDirs) {
|
|
157
|
+
if (!existsSync(extDir)) continue;
|
|
158
|
+
const entries = await readdir(extDir);
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
if (entry.startsWith(".")) continue;
|
|
161
|
+
const extPath = join(extDir, entry);
|
|
162
|
+
const stats = await stat(extPath);
|
|
163
|
+
if (!stats.isDirectory()) continue;
|
|
164
|
+
if (extensionName && entry !== extensionName) continue;
|
|
165
|
+
const packageJsonPath = join(extPath, "package.json");
|
|
166
|
+
if (existsSync(packageJsonPath)) {
|
|
167
|
+
result.push({ name: entry, path: extPath, hasPackageJson: true });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 检测当前目录是否为 spaceflow Extension
|
|
176
|
+
* 通过检查 package.json 中的 spaceflow 配置判断,不依赖固定目录名
|
|
177
|
+
*/
|
|
178
|
+
private detectCurrentExtension(): ExtensionInfo | null {
|
|
179
|
+
const cwd = process.cwd();
|
|
180
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
181
|
+
if (!existsSync(packageJsonPath)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const content = readFileSync(packageJsonPath, "utf-8");
|
|
186
|
+
const pkg = JSON.parse(content);
|
|
187
|
+
if (!pkg.spaceflow) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const name = cwd.split("/").pop() || "";
|
|
191
|
+
return { name, path: cwd, hasPackageJson: true };
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 构建单个 Extension
|
|
199
|
+
*/
|
|
200
|
+
private async buildExtension(
|
|
201
|
+
ext: ExtensionInfo,
|
|
202
|
+
verbose: VerboseLevel = 1,
|
|
203
|
+
): Promise<BuildResult> {
|
|
204
|
+
const startTime = Date.now();
|
|
205
|
+
if (shouldLog(verbose, 1)) console.log(t("build:building", { name: ext.name }));
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const config = await this.getConfig(ext);
|
|
209
|
+
const compiler = rspack(config);
|
|
210
|
+
|
|
211
|
+
const stats = await new Promise<Stats>((resolve, reject) => {
|
|
212
|
+
compiler.run((err, stats) => {
|
|
213
|
+
compiler.close((closeErr) => {
|
|
214
|
+
if (err) return reject(err);
|
|
215
|
+
if (closeErr) return reject(closeErr);
|
|
216
|
+
if (!stats) return reject(new Error("No stats returned"));
|
|
217
|
+
resolve(stats);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const duration = Date.now() - startTime;
|
|
223
|
+
const info = stats.toJson({ errors: true, warnings: true });
|
|
224
|
+
|
|
225
|
+
if (stats.hasErrors()) {
|
|
226
|
+
const errors = info.errors?.map((e) => e.message) || [];
|
|
227
|
+
console.log(t("build:buildFailedWithDuration", { duration }));
|
|
228
|
+
errors.forEach((e) => console.log(` ${e}`));
|
|
229
|
+
return { extension: ext.name, success: false, duration, errors };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (stats.hasWarnings()) {
|
|
233
|
+
const warnings = info.warnings?.map((w) => w.message) || [];
|
|
234
|
+
if (shouldLog(verbose, 1))
|
|
235
|
+
console.log(t("build:buildWarnings", { duration, count: warnings.length }));
|
|
236
|
+
return { extension: ext.name, success: true, duration, warnings };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (shouldLog(verbose, 1)) console.log(t("build:buildSuccess", { duration }));
|
|
240
|
+
return { extension: ext.name, success: true, duration };
|
|
241
|
+
} catch (error) {
|
|
242
|
+
const duration = Date.now() - startTime;
|
|
243
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
244
|
+
console.log(t("build:buildFailedWithMessage", { duration, message }));
|
|
245
|
+
return { extension: ext.name, success: false, duration, errors: [message] };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 监听单个 Extension
|
|
251
|
+
*/
|
|
252
|
+
private async watchExtension(ext: ExtensionInfo, verbose: VerboseLevel = 1): Promise<void> {
|
|
253
|
+
if (shouldLog(verbose, 1)) console.log(t("build:watching", { name: ext.name }));
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const config = await this.getConfig(ext);
|
|
257
|
+
const compiler = rspack(config);
|
|
258
|
+
|
|
259
|
+
this.watchers.set(ext.name, compiler);
|
|
260
|
+
|
|
261
|
+
compiler.watch({}, (err, stats) => {
|
|
262
|
+
if (err) {
|
|
263
|
+
console.log(t("build:watchError", { name: ext.name, message: err.message }));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!stats) return;
|
|
268
|
+
|
|
269
|
+
const info = stats.toJson({ errors: true, warnings: true });
|
|
270
|
+
|
|
271
|
+
if (stats.hasErrors()) {
|
|
272
|
+
console.log(t("build:watchBuildFailed", { name: ext.name }));
|
|
273
|
+
info.errors?.forEach((e) => console.log(` ${e.message}`));
|
|
274
|
+
} else if (stats.hasWarnings()) {
|
|
275
|
+
if (shouldLog(verbose, 1))
|
|
276
|
+
console.log(
|
|
277
|
+
t("build:watchBuildWarnings", { name: ext.name, count: info.warnings?.length }),
|
|
278
|
+
);
|
|
279
|
+
} else {
|
|
280
|
+
if (shouldLog(verbose, 1)) console.log(t("build:watchBuildSuccess", { name: ext.name }));
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
} catch (error) {
|
|
284
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
285
|
+
console.log(t("build:watchInitFailed", { name: ext.name, message }));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 获取 Extension 的 rspack 配置
|
|
291
|
+
*/
|
|
292
|
+
private async getConfig(ext: ExtensionInfo): Promise<Configuration> {
|
|
293
|
+
const customConfigPath = join(ext.path, "rspack.config.mjs");
|
|
294
|
+
const customConfigPathJs = join(ext.path, "rspack.config.js");
|
|
295
|
+
|
|
296
|
+
if (existsSync(customConfigPath)) {
|
|
297
|
+
const module = await import(customConfigPath);
|
|
298
|
+
return module.default || module;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (existsSync(customConfigPathJs)) {
|
|
302
|
+
const module = await import(customConfigPathJs);
|
|
303
|
+
return module.default || module;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return this.getDefaultConfig(ext);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 生成默认的 rspack 配置
|
|
311
|
+
*/
|
|
312
|
+
private getDefaultConfig(ext: ExtensionInfo): Configuration {
|
|
313
|
+
return createPluginConfig(
|
|
314
|
+
{
|
|
315
|
+
name: ext.name,
|
|
316
|
+
path: ext.path,
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
coreRoot: this.coreRoot,
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defineExtension, type VerboseLevel } from "@spaceflow/core";
|
|
2
|
+
import { BuildService } from "./build.service";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build 命令扩展
|
|
6
|
+
*/
|
|
7
|
+
export const buildExtension = defineExtension({
|
|
8
|
+
name: "build",
|
|
9
|
+
version: "1.0.0",
|
|
10
|
+
description: "构建 Extension 插件包",
|
|
11
|
+
commands: [
|
|
12
|
+
{
|
|
13
|
+
name: "build",
|
|
14
|
+
description: "构建指定或所有 Extension",
|
|
15
|
+
arguments: "[extension]",
|
|
16
|
+
options: [
|
|
17
|
+
{
|
|
18
|
+
flags: "-w, --watch",
|
|
19
|
+
description: "监听模式",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
flags: "-v, --verbose",
|
|
23
|
+
description: "详细输出",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
run: async (args, options, ctx) => {
|
|
27
|
+
const extensionName = args[0];
|
|
28
|
+
const verbose = (options?.verbose ? 2 : 1) as VerboseLevel;
|
|
29
|
+
const buildService = new BuildService();
|
|
30
|
+
try {
|
|
31
|
+
if (options?.watch) {
|
|
32
|
+
await buildService.watch(extensionName, verbose);
|
|
33
|
+
} else {
|
|
34
|
+
const results = await buildService.build(extensionName, verbose);
|
|
35
|
+
const hasErrors = results.some((r) => !r.success);
|
|
36
|
+
if (hasErrors) {
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
ctx.output.error(`构建失败: ${error instanceof Error ? error.message : error}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export * from "./build.service";
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { readFile, rm } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { existsSync, readdirSync, lstatSync } from "fs";
|
|
4
|
+
import { shouldLog, type VerboseLevel, t } from "@spaceflow/core";
|
|
5
|
+
import { getEditorDirName, DEFAULT_EDITOR } from "@spaceflow/core";
|
|
6
|
+
|
|
7
|
+
export class ClearService {
|
|
8
|
+
/**
|
|
9
|
+
* 获取支持的编辑器列表
|
|
10
|
+
*/
|
|
11
|
+
protected async getSupportedEditors(configPath: string): Promise<string[]> {
|
|
12
|
+
try {
|
|
13
|
+
if (!existsSync(configPath)) return [DEFAULT_EDITOR];
|
|
14
|
+
const content = await readFile(configPath, "utf-8");
|
|
15
|
+
const config = JSON.parse(content);
|
|
16
|
+
return config.support || [DEFAULT_EDITOR];
|
|
17
|
+
} catch {
|
|
18
|
+
return [DEFAULT_EDITOR];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 执行清理
|
|
24
|
+
*/
|
|
25
|
+
async execute(isGlobal = false, verbose: VerboseLevel = 1): Promise<void> {
|
|
26
|
+
if (shouldLog(verbose, 1)) {
|
|
27
|
+
if (isGlobal) {
|
|
28
|
+
console.log(t("clear:clearingGlobal"));
|
|
29
|
+
} else {
|
|
30
|
+
console.log(t("clear:clearing"));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cwd = process.cwd();
|
|
35
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
36
|
+
const configPath = join(cwd, "spaceflow.json");
|
|
37
|
+
|
|
38
|
+
// 1. 清理 .spaceflow/deps 目录
|
|
39
|
+
await this.clearSpaceflowDeps(isGlobal, verbose);
|
|
40
|
+
|
|
41
|
+
// 2. 清理各编辑器的 skills 和 commands 目录
|
|
42
|
+
const editors = await this.getSupportedEditors(configPath);
|
|
43
|
+
|
|
44
|
+
for (const editor of editors) {
|
|
45
|
+
const editorDirName = getEditorDirName(editor);
|
|
46
|
+
const editorRoot = isGlobal ? join(home, editorDirName) : join(cwd, editorDirName);
|
|
47
|
+
await this.clearEditorDir(editorRoot, editor, verbose);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (shouldLog(verbose, 1)) console.log(t("clear:clearDone"));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 清理 .spaceflow 目录内部文件(保留 spaceflow.json)
|
|
55
|
+
*/
|
|
56
|
+
private async clearSpaceflowDeps(isGlobal: boolean, verbose: VerboseLevel = 1): Promise<void> {
|
|
57
|
+
const cwd = process.cwd();
|
|
58
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
59
|
+
const spaceflowRoot = isGlobal ? join(home, ".spaceflow") : join(cwd, ".spaceflow");
|
|
60
|
+
|
|
61
|
+
if (!existsSync(spaceflowRoot)) {
|
|
62
|
+
if (shouldLog(verbose, 1)) console.log(t("clear:spaceflowNotExist"));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 需要保留的文件
|
|
67
|
+
const preserveFiles = ["spaceflow.json", "package.json"];
|
|
68
|
+
|
|
69
|
+
const entries = readdirSync(spaceflowRoot);
|
|
70
|
+
const toDelete = entries.filter((entry) => !preserveFiles.includes(entry));
|
|
71
|
+
|
|
72
|
+
if (toDelete.length === 0) {
|
|
73
|
+
if (shouldLog(verbose, 1)) console.log(t("clear:spaceflowNoClean"));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (shouldLog(verbose, 1))
|
|
78
|
+
console.log(t("clear:clearingSpaceflow", { count: toDelete.length }));
|
|
79
|
+
|
|
80
|
+
for (const entry of toDelete) {
|
|
81
|
+
const entryPath = join(spaceflowRoot, entry);
|
|
82
|
+
try {
|
|
83
|
+
await rm(entryPath, { recursive: true, force: true });
|
|
84
|
+
if (shouldLog(verbose, 2)) console.log(t("clear:deleted", { entry }));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (shouldLog(verbose, 1)) {
|
|
87
|
+
console.warn(
|
|
88
|
+
t("clear:deleteFailed", { entry }),
|
|
89
|
+
error instanceof Error ? error.message : error,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 清理编辑器目录下的 skills 和 commands
|
|
98
|
+
*/
|
|
99
|
+
private async clearEditorDir(
|
|
100
|
+
editorRoot: string,
|
|
101
|
+
editorName: string,
|
|
102
|
+
verbose: VerboseLevel = 1,
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
if (!existsSync(editorRoot)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 清理 skills 目录
|
|
109
|
+
const skillsDir = join(editorRoot, "skills");
|
|
110
|
+
if (existsSync(skillsDir)) {
|
|
111
|
+
const entries = readdirSync(skillsDir);
|
|
112
|
+
if (entries.length > 0) {
|
|
113
|
+
if (shouldLog(verbose, 1))
|
|
114
|
+
console.log(t("clear:clearingSkills", { editor: editorName, count: entries.length }));
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
const entryPath = join(skillsDir, entry);
|
|
117
|
+
try {
|
|
118
|
+
await rm(entryPath, { recursive: true, force: true });
|
|
119
|
+
if (shouldLog(verbose, 2)) console.log(t("clear:deleted", { entry }));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (shouldLog(verbose, 1)) {
|
|
122
|
+
console.warn(
|
|
123
|
+
t("clear:deleteFailed", { entry }),
|
|
124
|
+
error instanceof Error ? error.message : error,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 清理 commands 目录中的 .md 文件
|
|
133
|
+
const commandsDir = join(editorRoot, "commands");
|
|
134
|
+
if (existsSync(commandsDir)) {
|
|
135
|
+
const entries = readdirSync(commandsDir).filter((f) => f.endsWith(".md"));
|
|
136
|
+
if (entries.length > 0) {
|
|
137
|
+
if (shouldLog(verbose, 1))
|
|
138
|
+
console.log(t("clear:clearingCommands", { editor: editorName, count: entries.length }));
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
const entryPath = join(commandsDir, entry);
|
|
141
|
+
try {
|
|
142
|
+
const stats = lstatSync(entryPath);
|
|
143
|
+
if (stats.isFile()) {
|
|
144
|
+
await rm(entryPath);
|
|
145
|
+
if (shouldLog(verbose, 2)) console.log(t("clear:deleted", { entry }));
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (shouldLog(verbose, 1)) {
|
|
149
|
+
console.warn(
|
|
150
|
+
t("clear:deleteFailed", { entry }),
|
|
151
|
+
error instanceof Error ? error.message : error,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|