@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,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runx 命令参数解析工具
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface RunxParsedArgs {
|
|
6
|
+
cmdIndex: number;
|
|
7
|
+
sourceIndex: number;
|
|
8
|
+
source: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 查找 runx/x 命令在 argv 中的位置
|
|
14
|
+
*/
|
|
15
|
+
export function findRunxCmdIndex(argv: string[]): number {
|
|
16
|
+
return argv.findIndex((arg) => arg === "runx" || arg === "x");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 判断参数是否为 -n/--name 选项
|
|
21
|
+
*/
|
|
22
|
+
export function isNameOption(arg: string): boolean {
|
|
23
|
+
return arg === "-n" || arg === "--name" || arg.startsWith("-n=") || arg.startsWith("--name=");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 判断参数是否为 -n/--name 选项(需要跳过下一个参数)
|
|
28
|
+
*/
|
|
29
|
+
export function isNameOptionWithValue(arg: string): boolean {
|
|
30
|
+
return arg === "-n" || arg === "--name";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 从参数列表中找到 source(跳过 -n/--name 选项)
|
|
35
|
+
*/
|
|
36
|
+
export function findSourceInArgs(args: string[]): { index: number; source: string } {
|
|
37
|
+
for (let i = 0; i < args.length; i++) {
|
|
38
|
+
const arg = args[i];
|
|
39
|
+
if (isNameOptionWithValue(arg)) {
|
|
40
|
+
i++; // 跳过选项值
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (isNameOption(arg)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!arg.startsWith("-")) {
|
|
47
|
+
return { index: i, source: arg };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { index: -1, source: "" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 解析 runx 命令的完整参数
|
|
55
|
+
*/
|
|
56
|
+
export function parseRunxArgs(argv: string[]): RunxParsedArgs {
|
|
57
|
+
const cmdIndex = findRunxCmdIndex(argv);
|
|
58
|
+
if (cmdIndex === -1) {
|
|
59
|
+
return { cmdIndex: -1, sourceIndex: -1, source: "", args: [] };
|
|
60
|
+
}
|
|
61
|
+
const separatorIndex = argv.indexOf("--");
|
|
62
|
+
if (separatorIndex === -1) {
|
|
63
|
+
// 没有分隔符
|
|
64
|
+
const remaining = argv.slice(cmdIndex + 1);
|
|
65
|
+
const { index, source } = findSourceInArgs(remaining);
|
|
66
|
+
return {
|
|
67
|
+
cmdIndex,
|
|
68
|
+
sourceIndex: index === -1 ? -1 : cmdIndex + 1 + index,
|
|
69
|
+
source,
|
|
70
|
+
args: [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// 有分隔符
|
|
74
|
+
const beforeSeparator = argv.slice(cmdIndex + 1, separatorIndex);
|
|
75
|
+
const afterSeparator = argv.slice(separatorIndex + 1);
|
|
76
|
+
const { index, source } = findSourceInArgs(beforeSeparator);
|
|
77
|
+
return {
|
|
78
|
+
cmdIndex,
|
|
79
|
+
sourceIndex: index === -1 ? -1 : cmdIndex + 1 + index,
|
|
80
|
+
source,
|
|
81
|
+
args: afterSeparator,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineExtension, SchemaGeneratorService } from "@spaceflow/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Schema 命令扩展
|
|
5
|
+
*/
|
|
6
|
+
export const schemaExtension = defineExtension({
|
|
7
|
+
name: "schema",
|
|
8
|
+
version: "1.0.0",
|
|
9
|
+
description: "生成 schema",
|
|
10
|
+
commands: [
|
|
11
|
+
{
|
|
12
|
+
name: "schema",
|
|
13
|
+
description: "生成 JSON Schema 配置文件",
|
|
14
|
+
options: [
|
|
15
|
+
{
|
|
16
|
+
flags: "-o, --output <path>",
|
|
17
|
+
description: "输出路径",
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
run: async (_args, options, _ctx) => {
|
|
21
|
+
const schemaGenerator = new SchemaGeneratorService();
|
|
22
|
+
if (options?.output) {
|
|
23
|
+
schemaGenerator.generateJsonSchema(options.output as string);
|
|
24
|
+
} else {
|
|
25
|
+
schemaGenerator.generate();
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineExtension, SchemaGeneratorService } from "@spaceflow/core";
|
|
2
|
+
import { SetupService } from "./setup.service";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Setup 命令扩展
|
|
6
|
+
*/
|
|
7
|
+
export const setupExtension = defineExtension({
|
|
8
|
+
name: "setup",
|
|
9
|
+
version: "1.0.0",
|
|
10
|
+
description: "设置配置",
|
|
11
|
+
commands: [
|
|
12
|
+
{
|
|
13
|
+
name: "setup",
|
|
14
|
+
description: "设置配置",
|
|
15
|
+
options: [
|
|
16
|
+
{
|
|
17
|
+
flags: "-g, --global",
|
|
18
|
+
description: "全局设置",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
run: async (_args, options, _ctx) => {
|
|
22
|
+
const schemaGenerator = new SchemaGeneratorService();
|
|
23
|
+
const setupService = new SetupService(schemaGenerator);
|
|
24
|
+
if (options?.global) {
|
|
25
|
+
await setupService.setupGlobal();
|
|
26
|
+
} else {
|
|
27
|
+
await setupService.setupLocal();
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export * from "./setup.service";
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { t } from "@spaceflow/core";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import stringify from "json-stringify-pretty-compact";
|
|
6
|
+
import {
|
|
7
|
+
CONFIG_FILE_NAME,
|
|
8
|
+
RC_FILE_NAME,
|
|
9
|
+
getConfigPath,
|
|
10
|
+
readConfigSync,
|
|
11
|
+
type SpaceflowConfig,
|
|
12
|
+
SchemaGeneratorService,
|
|
13
|
+
SPACEFLOW_DIR,
|
|
14
|
+
ensureSpaceflowPackageJson,
|
|
15
|
+
} from "@spaceflow/core";
|
|
16
|
+
|
|
17
|
+
export class SetupService {
|
|
18
|
+
constructor(private readonly schemaGenerator: SchemaGeneratorService) {}
|
|
19
|
+
/**
|
|
20
|
+
* 本地初始化:创建 .spaceflow/ 目录和 package.json
|
|
21
|
+
*/
|
|
22
|
+
async setupLocal(): Promise<void> {
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
|
|
25
|
+
// 1. 创建 .spaceflow/ 目录和 package.json
|
|
26
|
+
const spaceflowDir = join(cwd, SPACEFLOW_DIR);
|
|
27
|
+
ensureSpaceflowPackageJson(spaceflowDir);
|
|
28
|
+
console.log(t("setup:dirCreated", { dir: spaceflowDir }));
|
|
29
|
+
|
|
30
|
+
// 2. 创建 spaceflow.json 配置文件(运行时配置)
|
|
31
|
+
const configPath = getConfigPath(cwd);
|
|
32
|
+
const rcPath = join(cwd, RC_FILE_NAME);
|
|
33
|
+
if (!existsSync(configPath) && !existsSync(rcPath)) {
|
|
34
|
+
this.schemaGenerator.generate();
|
|
35
|
+
const defaultConfig: Partial<SpaceflowConfig> = {
|
|
36
|
+
$schema: "./config-schema.json",
|
|
37
|
+
support: ["claudeCode"],
|
|
38
|
+
};
|
|
39
|
+
writeFileSync(configPath, stringify(defaultConfig, { indent: 2 }) + "\n");
|
|
40
|
+
console.log(t("setup:configGenerated", { path: configPath }));
|
|
41
|
+
} else {
|
|
42
|
+
const existingPath = existsSync(rcPath) ? rcPath : configPath;
|
|
43
|
+
console.log(t("setup:configExists", { path: existingPath }));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 全局初始化:创建 ~/.spaceflow/ 目录和 package.json,并合并配置
|
|
49
|
+
*/
|
|
50
|
+
async setupGlobal(): Promise<void> {
|
|
51
|
+
const cwd = process.cwd();
|
|
52
|
+
const globalDir = join(homedir(), SPACEFLOW_DIR);
|
|
53
|
+
const globalConfigPath = join(globalDir, CONFIG_FILE_NAME);
|
|
54
|
+
|
|
55
|
+
// 1. 创建 ~/.spaceflow/ 目录和 package.json
|
|
56
|
+
ensureSpaceflowPackageJson(globalDir);
|
|
57
|
+
console.log(t("setup:dirCreated", { dir: globalDir }));
|
|
58
|
+
|
|
59
|
+
// 读取本地配置(支持 .spaceflow/spaceflow.json 和 .spaceflowrc)
|
|
60
|
+
const localConfig = readConfigSync(cwd);
|
|
61
|
+
if (Object.keys(localConfig).length > 0) {
|
|
62
|
+
console.log(t("setup:localConfigRead"));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 读取本地 .env 文件并解析为配置
|
|
66
|
+
const envPath = join(cwd, ".env");
|
|
67
|
+
const envConfig = this.parseEnvToConfig(envPath);
|
|
68
|
+
|
|
69
|
+
const instanceConfig = (global as any).spaceflowConfig ?? {};
|
|
70
|
+
|
|
71
|
+
// 合并配置:本地配置(已含全局) < 实例配置 < 环境变量配置
|
|
72
|
+
const mergedConfig = this.deepMerge(localConfig, instanceConfig, envConfig);
|
|
73
|
+
|
|
74
|
+
// 写入全局配置
|
|
75
|
+
writeFileSync(globalConfigPath, stringify(mergedConfig, { indent: 2 }) + "\n");
|
|
76
|
+
console.log(t("setup:globalConfigGenerated", { path: globalConfigPath }));
|
|
77
|
+
|
|
78
|
+
// 显示合并的环境变量
|
|
79
|
+
if (Object.keys(envConfig).length > 0) {
|
|
80
|
+
console.log(t("setup:envConfigMerged"));
|
|
81
|
+
this.printConfigTree(envConfig, " ");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 解析 .env 文件为配置对象
|
|
87
|
+
* 支持嵌套格式:SPACEFLOW_GIT_PROVIDER_SERVER_URL -> { gitProvider: { serverUrl: "..." } }
|
|
88
|
+
*/
|
|
89
|
+
private parseEnvToConfig(envPath: string): Record<string, unknown> {
|
|
90
|
+
if (!existsSync(envPath)) {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const config: Record<string, unknown> = {};
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const content = readFileSync(envPath, "utf-8");
|
|
98
|
+
const lines = content.split("\n");
|
|
99
|
+
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
// 跳过空行和注释
|
|
103
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
104
|
+
|
|
105
|
+
const eqIndex = trimmed.indexOf("=");
|
|
106
|
+
if (eqIndex === -1) continue;
|
|
107
|
+
|
|
108
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
109
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
110
|
+
|
|
111
|
+
// 移除引号
|
|
112
|
+
if (
|
|
113
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
114
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
115
|
+
) {
|
|
116
|
+
value = value.slice(1, -1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 只处理 SPACEFLOW_ 前缀的环境变量
|
|
120
|
+
if (!key.startsWith("SPACEFLOW_")) continue;
|
|
121
|
+
|
|
122
|
+
// 转换为嵌套配置
|
|
123
|
+
// SPACEFLOW_GIT_PROVIDER_SERVER_URL -> gitProvider.serverUrl
|
|
124
|
+
const parts = key
|
|
125
|
+
.slice("SPACEFLOW_".length)
|
|
126
|
+
.toLowerCase()
|
|
127
|
+
.split("_")
|
|
128
|
+
.map((part, index) => {
|
|
129
|
+
// 第一个部分保持小写,后续部分转为 camelCase
|
|
130
|
+
if (index === 0) return part;
|
|
131
|
+
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// 重新组织为嵌套结构
|
|
135
|
+
// 例如: ["git", "Provider", "Server", "Url"] -> { gitProviderServerUrl: value }
|
|
136
|
+
// 简化处理:按照常见模式分组
|
|
137
|
+
this.setNestedValue(config, parts, value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(t("setup:envRead", { path: envPath }));
|
|
141
|
+
} catch {
|
|
142
|
+
console.warn(t("setup:envReadFailed", { path: envPath }));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return config;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 设置嵌套值
|
|
150
|
+
* 例如: ["review", "Gitea", "Server", "Url"] 和 value
|
|
151
|
+
* 结果: { review: { giteaServerUrl: value } }
|
|
152
|
+
*/
|
|
153
|
+
private setNestedValue(obj: Record<string, unknown>, parts: string[], value: string): void {
|
|
154
|
+
if (parts.length === 0) return;
|
|
155
|
+
|
|
156
|
+
// 第一个部分作为顶级 key(如 review, commit 等)
|
|
157
|
+
const topKey = parts[0];
|
|
158
|
+
|
|
159
|
+
if (parts.length === 1) {
|
|
160
|
+
obj[topKey] = value;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 剩余部分合并为 camelCase 作为嵌套 key
|
|
165
|
+
// 例如: ["Gitea", "Server", "Url"] -> giteaServerUrl
|
|
166
|
+
const restParts = parts.slice(1);
|
|
167
|
+
const nestedKey = restParts
|
|
168
|
+
.map((part, index) => (index === 0 ? part.toLowerCase() : part))
|
|
169
|
+
.join("");
|
|
170
|
+
|
|
171
|
+
if (!obj[topKey] || typeof obj[topKey] !== "object") {
|
|
172
|
+
obj[topKey] = {};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
(obj[topKey] as Record<string, unknown>)[nestedKey] = value;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 深度合并对象
|
|
180
|
+
*/
|
|
181
|
+
private deepMerge<T extends Record<string, unknown>>(...objects: Partial<T>[]): Partial<T> {
|
|
182
|
+
const result: Record<string, unknown> = {};
|
|
183
|
+
|
|
184
|
+
for (const obj of objects) {
|
|
185
|
+
for (const key in obj) {
|
|
186
|
+
const value = obj[key];
|
|
187
|
+
const existing = result[key];
|
|
188
|
+
|
|
189
|
+
if (
|
|
190
|
+
value !== null &&
|
|
191
|
+
typeof value === "object" &&
|
|
192
|
+
!Array.isArray(value) &&
|
|
193
|
+
existing !== null &&
|
|
194
|
+
typeof existing === "object" &&
|
|
195
|
+
!Array.isArray(existing)
|
|
196
|
+
) {
|
|
197
|
+
result[key] = this.deepMerge(
|
|
198
|
+
existing as Record<string, unknown>,
|
|
199
|
+
value as Record<string, unknown>,
|
|
200
|
+
);
|
|
201
|
+
} else if (value !== undefined) {
|
|
202
|
+
result[key] = value;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return result as Partial<T>;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 打印配置树
|
|
212
|
+
*/
|
|
213
|
+
private printConfigTree(config: Record<string, unknown>, prefix: string): void {
|
|
214
|
+
for (const [key, value] of Object.entries(config)) {
|
|
215
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
216
|
+
console.log(`${prefix}${key}:`);
|
|
217
|
+
this.printConfigTree(value as Record<string, unknown>, prefix + " ");
|
|
218
|
+
} else {
|
|
219
|
+
// 隐藏敏感值
|
|
220
|
+
const displayValue = this.isSensitiveKey(key) ? "***" : String(value);
|
|
221
|
+
console.log(`${prefix}${key}: ${displayValue}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 判断是否为敏感 key
|
|
228
|
+
*/
|
|
229
|
+
private isSensitiveKey(key: string): boolean {
|
|
230
|
+
const sensitivePatterns = ["token", "secret", "password", "key", "apikey"];
|
|
231
|
+
const lowerKey = key.toLowerCase();
|
|
232
|
+
return sensitivePatterns.some((pattern) => lowerKey.includes(pattern));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineExtension, type VerboseLevel } from "@spaceflow/core";
|
|
2
|
+
import { UninstallService } from "./uninstall.service";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Uninstall 命令扩展
|
|
6
|
+
*/
|
|
7
|
+
export const uninstallExtension = defineExtension({
|
|
8
|
+
name: "uninstall",
|
|
9
|
+
version: "1.0.0",
|
|
10
|
+
description: "卸载 Extension",
|
|
11
|
+
commands: [
|
|
12
|
+
{
|
|
13
|
+
name: "uninstall",
|
|
14
|
+
description: "卸载 Extension",
|
|
15
|
+
aliases: ["un"],
|
|
16
|
+
arguments: "<name>",
|
|
17
|
+
options: [
|
|
18
|
+
{
|
|
19
|
+
flags: "-g, --global",
|
|
20
|
+
description: "全局卸载",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
flags: "-v, --verbose",
|
|
24
|
+
description: "详细输出",
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
run: async (args, options, ctx) => {
|
|
28
|
+
const name = args[0];
|
|
29
|
+
if (!name) {
|
|
30
|
+
ctx.output.error("请指定要卸载的扩展名称");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const verbose = (options?.verbose ? 2 : 1) as VerboseLevel;
|
|
34
|
+
const isGlobal = !!options?.global;
|
|
35
|
+
const uninstallService = new UninstallService();
|
|
36
|
+
await uninstallService.execute(name, isGlobal, verbose);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export * from "./uninstall.service";
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { readFile, rm } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { existsSync, readdirSync } from "fs";
|
|
5
|
+
import { shouldLog, type VerboseLevel, t } from "@spaceflow/core";
|
|
6
|
+
import { getEditorDirName } from "@spaceflow/core";
|
|
7
|
+
import { detectPackageManager } from "@spaceflow/core";
|
|
8
|
+
import { getSpaceflowDir } from "@spaceflow/core";
|
|
9
|
+
import { getConfigPath, getSupportedEditors, removeDependency } from "@spaceflow/core";
|
|
10
|
+
|
|
11
|
+
export class UninstallService {
|
|
12
|
+
/**
|
|
13
|
+
* 从 .spaceflow/node_modules/ 目录卸载 Extension
|
|
14
|
+
* 所有类型的 Extension 都通过 pnpm remove 卸载
|
|
15
|
+
*/
|
|
16
|
+
private async uninstallExtension(
|
|
17
|
+
name: string,
|
|
18
|
+
isGlobal: boolean = false,
|
|
19
|
+
verbose: VerboseLevel = 1,
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
const spaceflowDir = getSpaceflowDir(isGlobal);
|
|
22
|
+
|
|
23
|
+
if (shouldLog(verbose, 1)) {
|
|
24
|
+
console.log(t("uninstall:uninstallingExtension", { name }));
|
|
25
|
+
console.log(t("uninstall:targetDir", { dir: spaceflowDir }));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const pm = detectPackageManager(spaceflowDir);
|
|
29
|
+
let cmd: string;
|
|
30
|
+
if (pm === "pnpm") {
|
|
31
|
+
cmd = `pnpm remove --prefix "${spaceflowDir}" ${name}`;
|
|
32
|
+
} else {
|
|
33
|
+
cmd = `npm uninstall --prefix "${spaceflowDir}" ${name}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
execSync(cmd, {
|
|
38
|
+
cwd: process.cwd(),
|
|
39
|
+
stdio: verbose ? "inherit" : "pipe",
|
|
40
|
+
});
|
|
41
|
+
} catch {
|
|
42
|
+
if (shouldLog(verbose, 1)) console.warn(t("uninstall:extensionUninstallFailed", { name }));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 执行卸载
|
|
48
|
+
*/
|
|
49
|
+
async execute(name: string, isGlobal = false, verbose: VerboseLevel = 1): Promise<void> {
|
|
50
|
+
if (shouldLog(verbose, 1)) {
|
|
51
|
+
if (isGlobal) {
|
|
52
|
+
console.log(t("uninstall:uninstallingGlobal", { name }));
|
|
53
|
+
} else {
|
|
54
|
+
console.log(t("uninstall:uninstalling", { name }));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const cwd = process.cwd();
|
|
59
|
+
const configPath = getConfigPath(cwd);
|
|
60
|
+
|
|
61
|
+
// 1. 读取配置获取 source
|
|
62
|
+
const dependencies = await this.parseSkillsFromConfig(configPath);
|
|
63
|
+
let actualName = name;
|
|
64
|
+
let config = dependencies[name];
|
|
65
|
+
|
|
66
|
+
// 如果通过 name 找不到,尝试通过 source 值查找(支持 @spaceflow/review 这样的 npm 包名)
|
|
67
|
+
if (!config) {
|
|
68
|
+
for (const [key, value] of Object.entries(dependencies)) {
|
|
69
|
+
if (value === name) {
|
|
70
|
+
actualName = key;
|
|
71
|
+
config = value;
|
|
72
|
+
if (shouldLog(verbose, 1)) console.log(t("uninstall:foundDependency", { key, value }));
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!config && !isGlobal) {
|
|
79
|
+
throw new Error(t("uninstall:notRegistered", { name }));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 使用实际的 name 进行后续操作
|
|
83
|
+
name = actualName;
|
|
84
|
+
|
|
85
|
+
// 2. 从 .spaceflow/node_modules/ 卸载 Extension
|
|
86
|
+
await this.uninstallExtension(name, isGlobal, verbose);
|
|
87
|
+
|
|
88
|
+
// 3. 删除各个编辑器 commands/skills 中的复制文件
|
|
89
|
+
const editors = getSupportedEditors(cwd);
|
|
90
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
91
|
+
|
|
92
|
+
for (const editor of editors) {
|
|
93
|
+
const editorDirName = getEditorDirName(editor);
|
|
94
|
+
const installRoot = isGlobal ? join(home, editorDirName) : join(cwd, editorDirName);
|
|
95
|
+
await this.removeEditorFiles(installRoot, name, verbose);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 4. 从配置文件中移除(仅本地安装)
|
|
99
|
+
if (!isGlobal && config) {
|
|
100
|
+
this.removeFromConfig(name, cwd, verbose);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (shouldLog(verbose, 1)) console.log(t("uninstall:uninstallDone"));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 删除编辑器目录中的 commands/skills 文件
|
|
108
|
+
* install 现在是复制文件到编辑器目录,所以卸载时需要删除这些复制的文件/目录
|
|
109
|
+
*/
|
|
110
|
+
private async removeEditorFiles(
|
|
111
|
+
installRoot: string,
|
|
112
|
+
name: string,
|
|
113
|
+
verbose: VerboseLevel = 1,
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
// 删除 skills 目录中的文件
|
|
116
|
+
const skillsDir = join(installRoot, "skills");
|
|
117
|
+
if (existsSync(skillsDir)) {
|
|
118
|
+
const entries = readdirSync(skillsDir);
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
// 匹配 name 或 name-xxx 格式
|
|
121
|
+
if (entry === name || entry.startsWith(`${name}-`)) {
|
|
122
|
+
const targetPath = join(skillsDir, entry);
|
|
123
|
+
if (shouldLog(verbose, 1)) console.log(t("uninstall:deletingSkill", { entry }));
|
|
124
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 删除 commands 目录中的 .md 文件
|
|
130
|
+
const commandsDir = join(installRoot, "commands");
|
|
131
|
+
if (existsSync(commandsDir)) {
|
|
132
|
+
const entries = readdirSync(commandsDir);
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
// 匹配 name.md 或 name-xxx.md 格式
|
|
135
|
+
if (entry === `${name}.md` || entry.startsWith(`${name}-`)) {
|
|
136
|
+
const targetPath = join(commandsDir, entry);
|
|
137
|
+
if (shouldLog(verbose, 1)) console.log(t("uninstall:deletingCommand", { entry }));
|
|
138
|
+
await rm(targetPath, { force: true });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 从配置文件解析 dependencies
|
|
146
|
+
*/
|
|
147
|
+
private async parseSkillsFromConfig(configPath: string): Promise<Record<string, unknown>> {
|
|
148
|
+
try {
|
|
149
|
+
const content = await readFile(configPath, "utf-8");
|
|
150
|
+
const config = JSON.parse(content);
|
|
151
|
+
return config.dependencies || {};
|
|
152
|
+
} catch {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 从配置文件中移除依赖
|
|
159
|
+
*/
|
|
160
|
+
private removeFromConfig(name: string, cwd: string, verbose: VerboseLevel = 1): void {
|
|
161
|
+
const removed = removeDependency(name, cwd);
|
|
162
|
+
if (removed && shouldLog(verbose, 1)) {
|
|
163
|
+
console.log(t("uninstall:configUpdated", { path: getConfigPath(cwd) }));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineExtension, type VerboseLevel } from "@spaceflow/core";
|
|
2
|
+
import { UpdateService } from "./update.service";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Update 命令扩展
|
|
6
|
+
*/
|
|
7
|
+
export const updateExtension = defineExtension({
|
|
8
|
+
name: "update",
|
|
9
|
+
version: "1.0.0",
|
|
10
|
+
description: "更新 Extension",
|
|
11
|
+
commands: [
|
|
12
|
+
{
|
|
13
|
+
name: "update",
|
|
14
|
+
description: "更新 Extension",
|
|
15
|
+
arguments: "[name]",
|
|
16
|
+
options: [
|
|
17
|
+
{
|
|
18
|
+
flags: "-a, --all",
|
|
19
|
+
description: "更新所有扩展",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
flags: "-v, --verbose",
|
|
23
|
+
description: "详细输出",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
run: async (args, options, _ctx) => {
|
|
27
|
+
const name = args[0];
|
|
28
|
+
const verbose = (options?.verbose ? 2 : 1) as VerboseLevel;
|
|
29
|
+
const updateService = new UpdateService();
|
|
30
|
+
if (options?.all) {
|
|
31
|
+
await updateService.updateAll(verbose);
|
|
32
|
+
} else if (name) {
|
|
33
|
+
await updateService.updateDependency(name, verbose);
|
|
34
|
+
} else {
|
|
35
|
+
await updateService.updateSelf(verbose);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export * from "./update.service";
|