chore: 初始化仓库

This commit is contained in:
Lydanne
2026-02-15 22:02:21 +08:00
commit 08d011d63f
381 changed files with 87202 additions and 0 deletions

94
cli/CHANGELOG.md Normal file
View File

@@ -0,0 +1,94 @@
# Changelog
## [0.19.0](https://git.bjxgj.com/xgj/spaceflow/compare/@spaceflow/cli@0.18.0...@spaceflow/cli@0.19.0) (2026-02-15)
### 新特性
* **cli:** 新增 MCP Server 命令并集成 review 扩展的 MCP 工具 ([b794b36](https://git.bjxgj.com/xgj/spaceflow/commit/b794b36d90788c7eb4cbb253397413b4a080ae83))
* **cli:** 新增 MCP Server 导出类型支持 ([9568cbd](https://git.bjxgj.com/xgj/spaceflow/commit/9568cbd14d4cfbdedaf2218379c72337af6db271))
* **core:** 为所有命令添加 i18n 国际化支持 ([867c5d3](https://git.bjxgj.com/xgj/spaceflow/commit/867c5d3eccc285c8a68803b8aa2f0ffb86a94285))
* **core:** 新增 GitLab 平台适配器并完善配置支持 ([47be9ad](https://git.bjxgj.com/xgj/spaceflow/commit/47be9adfa90944a9cb183e03286a7a96fec747f1))
* **core:** 新增 Logger 全局日志工具并支持 plain/tui 双模式渲染 ([8baae7c](https://git.bjxgj.com/xgj/spaceflow/commit/8baae7c24139695a0e379e1c874023cd61dfc41b))
* **docs:** 新增 VitePress 文档站点并完善项目文档 ([a79d620](https://git.bjxgj.com/xgj/spaceflow/commit/a79d6208e60390a44fa4c94621eb41ae20159e98))
* **mcp:** 新增 MCP Inspector 交互式调试支持并优化工具日志输出 ([05fd2ee](https://git.bjxgj.com/xgj/spaceflow/commit/05fd2ee941c5f6088b769d1127cb7c0615626f8c))
* **review:** 为 MCP 服务添加 i18n 国际化支持 ([a749054](https://git.bjxgj.com/xgj/spaceflow/commit/a749054eb73b775a5f5973ab1b86c04f2b2ddfba))
* **review:** 新增规则级 includes 解析测试并修复文件级/规则级 includes 过滤逻辑 ([4baca71](https://git.bjxgj.com/xgj/spaceflow/commit/4baca71c17782fb92a95b3207f9c61e0b410b9ff))
### 修复BUG
* **actions:** 修正 pnpm setup 命令调用方式 ([8f014fa](https://git.bjxgj.com/xgj/spaceflow/commit/8f014fa90b74e20de4c353804d271b3ef6f1288f))
* **mcp:** 添加 -y 选项确保 Inspector 自动安装依赖 ([a9201f7](https://git.bjxgj.com/xgj/spaceflow/commit/a9201f74bd9ddc5eba92beaaa676f377842863e0))
### 代码重构
* **claude:** 移除 .claude 目录及其 .gitignore 配置文件 ([91916a9](https://git.bjxgj.com/xgj/spaceflow/commit/91916a99f65da31c1d34e6f75b5cbea1d331ba35))
* **cli:** 优化依赖安装流程并支持 .spaceflow 目录配置 ([5977631](https://git.bjxgj.com/xgj/spaceflow/commit/597763183eaa61bb024bba2703d75239650b54fb))
* **cli:** 拆分 CLI 为独立包并重构扩展加载机制 ([b385d28](https://git.bjxgj.com/xgj/spaceflow/commit/b385d281575f29b823bb6dc4229a396a29c0e226))
* **cli:** 移除 ExtensionModule 并优化扩展加载机制 ([8f7077d](https://git.bjxgj.com/xgj/spaceflow/commit/8f7077deaef4e5f4032662ff5ac925cd3c07fdb6))
* **cli:** 调整依赖顺序并格式化导入语句 ([32a9c1c](https://git.bjxgj.com/xgj/spaceflow/commit/32a9c1cf834725a20f93b1f8f60b52692841a3e5))
* **cli:** 重构 getPluginConfigFromPackageJson 方法以提高代码可读性 ([f5f6ed9](https://git.bjxgj.com/xgj/spaceflow/commit/f5f6ed9858cc4ca670e30fac469774bdc8f7b005))
* **cli:** 重构扩展配置格式,支持 flow/command/skill 三种导出类型 ([958dc13](https://git.bjxgj.com/xgj/spaceflow/commit/958dc130621f78bbcc260224da16a5f16ae0b2b1))
* **core:** 为 build/clear/commit 命令添加国际化支持 ([de82cb2](https://git.bjxgj.com/xgj/spaceflow/commit/de82cb2f1ed8cef0e446a2d42a1bf1f091e9c421))
* **core:** 优化 list 命令输出格式并修复 MCP Inspector 包管理器兼容性 ([a019829](https://git.bjxgj.com/xgj/spaceflow/commit/a019829d3055c083aeb86ed60ce6629d13012d91))
* **core:** 将 rspack 配置和工具函数中的 @spaceflow/cli 引用改为 @spaceflow/core ([3c301c6](https://git.bjxgj.com/xgj/spaceflow/commit/3c301c60f3e61b127db94481f5a19307f5ef00eb))
* **core:** 将扩展依赖从 @spaceflow/cli 迁移到 @spaceflow/core ([6f9ffd4](https://git.bjxgj.com/xgj/spaceflow/commit/6f9ffd4061cecae4faaf3d051e3ca98a0b42b01f))
* **core:** 提取 source 处理和包管理器工具函数到共享模块 ([ab3ff00](https://git.bjxgj.com/xgj/spaceflow/commit/ab3ff003d1cd586c0c4efc7841e6a93fe3477ace))
* **core:** 新增 getEnvFilePaths 工具函数统一管理 .env 文件路径优先级 ([809fa18](https://git.bjxgj.com/xgj/spaceflow/commit/809fa18f3d0b8eabcb068988bab53d548eaf03ea))
* **core:** 新增远程仓库规则拉取功能并支持 Git API 获取目录内容 ([69ade16](https://git.bjxgj.com/xgj/spaceflow/commit/69ade16c9069f9e1a90b3ef56dc834e33a3c0650))
* **core:** 统一 LogLevel 类型定义并支持字符串/数字双模式 ([557f6b0](https://git.bjxgj.com/xgj/spaceflow/commit/557f6b0bc39fcfb0e3f773836cbbf08c1a8790ae))
* **core:** 重构配置读取逻辑,新增 ConfigReaderService 并支持 .spaceflowrc 配置文件 ([72e88ce](https://git.bjxgj.com/xgj/spaceflow/commit/72e88ced63d03395923cdfb113addf4945162e54))
* **i18n:** 将 locales 导入从命令文件迁移至扩展入口文件 ([0da5d98](https://git.bjxgj.com/xgj/spaceflow/commit/0da5d9886296c4183b24ad8c56140763f5a870a4))
* **i18n:** 移除扩展元数据中的 locales 字段并改用 side-effect 自动注册 ([2c7d488](https://git.bjxgj.com/xgj/spaceflow/commit/2c7d488a9dfa59a99b95e40e3c449c28c2d433d8))
* **mcp:** 使用 DTO + Swagger 装饰器替代手动 JSON Schema 定义 ([87ec262](https://git.bjxgj.com/xgj/spaceflow/commit/87ec26252dd295536bb090ae8b7e418eec96e1bd))
* **mcp:** 升级 MCP SDK API 并优化 Inspector 调试配置 ([176d04a](https://git.bjxgj.com/xgj/spaceflow/commit/176d04a73fbbb8d115520d922f5fedb9a2961aa6))
* **mcp:** 将 MCP 元数据存储从 Reflect Metadata 改为静态属性以支持跨模块访问 ([cac0ea2](https://git.bjxgj.com/xgj/spaceflow/commit/cac0ea2029e1b504bc4278ce72b3aa87fff88c84))
* **test:** 迁移测试框架从 Jest 到 Vitest ([308f9d4](https://git.bjxgj.com/xgj/spaceflow/commit/308f9d49089019530588344a5e8880f5b6504a6a))
* 优化构建流程并调整 MCP/review 日志输出级别 ([74072c0](https://git.bjxgj.com/xgj/spaceflow/commit/74072c04be7a45bfc0ab53b636248fe5c0e1e42a))
* 将 .spaceflow/package.json 纳入版本控制并自动添加到根项目依赖 ([ab83d25](https://git.bjxgj.com/xgj/spaceflow/commit/ab83d2579cb5414ee3d78a9768fac2147a3d1ad9))
* 将 GiteaSdkModule/GiteaSdkService 重命名为 GitProviderModule/GitProviderService ([462f492](https://git.bjxgj.com/xgj/spaceflow/commit/462f492bc2607cf508c5011d181c599cf17e00c9))
* 恢复 pnpm catalog 配置并移除 .spaceflow 工作区导入器 ([217387e](https://git.bjxgj.com/xgj/spaceflow/commit/217387e2e8517a08162e9bcaf604893fd9bca736))
* 迁移扩展依赖到 .spaceflow 工作区并移除 pnpm catalog ([c457c0f](https://git.bjxgj.com/xgj/spaceflow/commit/c457c0f8918171f1856b88bc007921d76c508335))
* 重构 Extension 安装机制为 pnpm workspace 模式 ([469b12e](https://git.bjxgj.com/xgj/spaceflow/commit/469b12eac28f747b628e52a5125a3d5a538fba39))
* 重构插件加载改为扩展模式 ([0e6e140](https://git.bjxgj.com/xgj/spaceflow/commit/0e6e140b19ea2cf6084afc261c555d2083fe04f9))
### 文档更新
* **guide:** 更新编辑器集成文档,补充四种导出类型说明和 MCP 注册机制 ([19a7409](https://git.bjxgj.com/xgj/spaceflow/commit/19a7409092c89d002f11ee51ebcb6863118429bd))
* **guide:** 更新配置文件位置说明并补充 RC 文件支持 ([2214dc4](https://git.bjxgj.com/xgj/spaceflow/commit/2214dc4e197221971f5286b38ceaa6fcbcaa7884))
### 测试用例
* **core:** 新增 GiteaAdapter 完整单元测试并实现自动检测 provider 配置 ([c74f745](https://git.bjxgj.com/xgj/spaceflow/commit/c74f7458aed91ac7d12fb57ef1c24b3d2917c406))
* **review:** 新增 DeletionImpactService 测试覆盖并配置 coverage 工具 ([50bfbfe](https://git.bjxgj.com/xgj/spaceflow/commit/50bfbfe37192641f1170ade8f5eb00e0e382af67))
### 其他修改
* **ci-scripts:** released version 0.18.0 [no ci] ([e17894a](https://git.bjxgj.com/xgj/spaceflow/commit/e17894a5af53ff040a0a17bc602d232f78415e1b))
* **ci-shell:** released version 0.18.0 [no ci] ([f64fd80](https://git.bjxgj.com/xgj/spaceflow/commit/f64fd8009a6dd725f572c7e9fbf084d9320d5128))
* **ci:** 迁移工作流从 Gitea 到 GitHub 并统一环境变量命名 ([57e3bae](https://git.bjxgj.com/xgj/spaceflow/commit/57e3bae635b324c8c4ea50a9fb667b6241fae0ef))
* **config:** 将 git 推送白名单用户从 "Gitea Actions" 改为 "GiteaActions" ([fdbb865](https://git.bjxgj.com/xgj/spaceflow/commit/fdbb865341e6f02b26fca32b54a33b51bee11cad))
* **config:** 将 git 推送白名单用户从 github-actions[bot] 改为 Gitea Actions ([9c39819](https://git.bjxgj.com/xgj/spaceflow/commit/9c39819a9f95f415068f7f0333770b92bc98321b))
* **config:** 移除 review-spec 私有仓库依赖 ([8ae18f1](https://git.bjxgj.com/xgj/spaceflow/commit/8ae18f13c441b033d1cbc75119695a5cc5cb6a0b))
* **core:** released version 0.1.0 [no ci] ([170fa67](https://git.bjxgj.com/xgj/spaceflow/commit/170fa670e98473c2377120656d23aae835c51997))
* **core:** 禁用 i18next 初始化时的 locize.com 推广日志 ([a99fbb0](https://git.bjxgj.com/xgj/spaceflow/commit/a99fbb068441bc623efcf15a1dd7b6bd38c05f38))
* **deps:** 移除 pnpm catalog 配置并更新依赖锁定 ([753fb9e](https://git.bjxgj.com/xgj/spaceflow/commit/753fb9e3e43b28054c75158193dc39ab4bab1af5))
* **docs:** 统一文档脚本命名,为 VitePress 命令添加 docs: 前缀 ([3cc46ea](https://git.bjxgj.com/xgj/spaceflow/commit/3cc46eab3a600290f5064b8270902e586b9c5af4))
* **i18n:** 配置 i18n-ally-next 自动提取键名生成策略 ([753c3dc](https://git.bjxgj.com/xgj/spaceflow/commit/753c3dc3f24f3c03c837d1ec2c505e8e3ce08b11))
* **i18n:** 重构 i18n 配置并统一 locales 目录结构 ([3e94037](https://git.bjxgj.com/xgj/spaceflow/commit/3e94037fa6493b3b0e4a12ff6af9f4bea48ae217))
* **period-summary:** released version 0.18.0 [no ci] ([f0df638](https://git.bjxgj.com/xgj/spaceflow/commit/f0df63804d06f8c75e04169ec98226d7a4f5d7f9))
* **publish:** released version 0.20.0 [no ci] ([d347e3b](https://git.bjxgj.com/xgj/spaceflow/commit/d347e3b2041157d8dc6e3ade69b05a481b2ab371))
* **review:** released version 0.28.0 [no ci] ([a2d89ed](https://git.bjxgj.com/xgj/spaceflow/commit/a2d89ed5f386eb6dd299c0d0a208856ce267ab5e))
* **scripts:** 修正 setup 和 build 脚本的过滤条件,避免重复构建 cli 包 ([ffd2ffe](https://git.bjxgj.com/xgj/spaceflow/commit/ffd2ffedca08fd56cccb6a9fbd2b6bd106e367b6))
* **templates:** 新增 MCP 工具插件模板 ([5f6df60](https://git.bjxgj.com/xgj/spaceflow/commit/5f6df60b60553f025414fd102d8a279cde097485))
* **workflows:** 为所有 GitHub Actions 工作流添加 GIT_PROVIDER_TYPE 环境变量 ([a463574](https://git.bjxgj.com/xgj/spaceflow/commit/a463574de6755a0848a8d06267f029cb947132b0))
* **workflows:** 在发布流程中添加 GIT_PROVIDER_TYPE 环境变量 ([a4bb388](https://git.bjxgj.com/xgj/spaceflow/commit/a4bb3881f39ad351e06c5502df6895805b169a28))
* **workflows:** 在发布流程中添加扩展安装步骤 ([716be4d](https://git.bjxgj.com/xgj/spaceflow/commit/716be4d92641ccadb3eaf01af8a51189ec5e9ade))
* **workflows:** 将发布流程的 Git 和 NPM 配置从 GitHub 迁移到 Gitea ([6d9acff](https://git.bjxgj.com/xgj/spaceflow/commit/6d9acff06c9a202432eb3d3d5552e6ac972712f5))
* **workflows:** 将发布流程的 GITHUB_TOKEN 改为使用 CI_GITEA_TOKEN ([e7fe7b4](https://git.bjxgj.com/xgj/spaceflow/commit/e7fe7b4271802fcdbfc2553b180f710eed419335))
* 为所有 commands 包添加 @spaceflow/cli 开发依赖 ([d4e6c83](https://git.bjxgj.com/xgj/spaceflow/commit/d4e6c8344ca736f7e55d7db698482e8fa2445684))
* 优化依赖配置并移除 .spaceflow 包依赖 ([be5264e](https://git.bjxgj.com/xgj/spaceflow/commit/be5264e5e0fe1f53bbe3b44a9cb86dd94ab9d266))
* 修正 postinstall 脚本命令格式 ([3f0820f](https://git.bjxgj.com/xgj/spaceflow/commit/3f0820f85dee88808de921c3befe2d332f34cc36))
* 恢复 pnpm catalog 配置并更新依赖锁定 ([0b2295c](https://git.bjxgj.com/xgj/spaceflow/commit/0b2295c1f906d89ad3ba7a61b04c6e6b94f193ef))
* 新增 .spaceflow/pnpm-workspace.yaml 防止被父级 workspace 接管并移除根项目 devDependencies 自动添加逻辑 ([61de3a2](https://git.bjxgj.com/xgj/spaceflow/commit/61de3a2b75e8a19b28563d2a6476158d19f6c5be))
* 新增 postinstall 钩子自动执行 setup 脚本 ([64dae0c](https://git.bjxgj.com/xgj/spaceflow/commit/64dae0cb440bd5e777cb790f826ff2d9f8fe65ba))
* 移除 postinstall 钩子避免依赖安装时自动执行构建 ([ea1dc85](https://git.bjxgj.com/xgj/spaceflow/commit/ea1dc85ce7d6cf23a98c13e2c21e3c3bcdf7dd79))

50
cli/package.json Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "@spaceflow/cli",
"version": "0.19.0",
"description": "Spaceflow CLI 工具",
"license": "UNLICENSED",
"author": "",
"bin": {
"space": "dist/cli.js",
"spaceflow": "dist/cli.js"
},
"type": "module",
"scripts": {
"build": "rspack build -c rspack.config.mjs",
"format": "oxfmt src --write",
"lint": "oxlint src",
"test": "vitest run",
"test:watch": "vitest",
"test:cov": "vitest run --coverage"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.26.0",
"@rspack/cli": "^1.7.4",
"@rspack/core": "^1.7.4",
"@spaceflow/core": "workspace:*",
"json-stringify-pretty-compact": "^4.0.0",
"micromatch": "^4.0.8",
"zod-to-json-schema": "^3.25.1"
},
"peerDependencies": {
"@nestjs/common": "catalog:",
"@nestjs/config": "catalog:",
"@nestjs/core": "catalog:",
"nest-commander": "catalog:",
"reflect-metadata": "catalog:",
"rxjs": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@nestjs/cli": "catalog:",
"@nestjs/schematics": "catalog:",
"@nestjs/testing": "catalog:",
"@types/node": "catalog:",
"ts-node": "catalog:",
"tsconfig-paths": "catalog:",
"typescript": "catalog:",
"typescript-eslint": "catalog:",
"@vitest/coverage-v8": "catalog:",
"vitest": "catalog:"
}
}

70
cli/rspack.config.mjs Normal file
View File

@@ -0,0 +1,70 @@
import { fileURLToPath } from "url";
import { dirname, resolve } from "path";
import rspack from "@rspack/core";
const __dirname = dirname(fileURLToPath(import.meta.url));
export default {
optimization: {
minimize: false,
},
entry: {
cli: "./src/cli.ts",
},
plugins: [
new rspack.BannerPlugin({
banner: "#!/usr/bin/env node",
raw: true,
entryOnly: true,
include: /cli\.js$/,
}),
],
target: "node",
mode: process.env.NODE_ENV === "production" ? "production" : "development",
output: {
filename: "[name].js",
path: resolve(__dirname, "dist"),
library: { type: "module" },
chunkFormat: "module",
clean: true,
},
experiments: {
outputModule: true,
},
externalsType: "module-import",
externals: [
{ micromatch: "node-commonjs micromatch" },
/^(?!src\/)[^./]/, // node_modules 包(排除 src/ 别名)
],
resolve: {
extensions: [".ts", ".js"],
extensionAlias: {
".js": [".ts", ".js"],
},
tsConfig: {
configFile: resolve(__dirname, "tsconfig.json"),
},
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
loader: "builtin:swc-loader",
options: {
jsc: {
parser: {
syntax: "typescript",
decorators: true,
},
transform: {
legacyDecorator: true,
decoratorMetadata: true,
},
target: "es2022",
},
},
},
],
},
};

28
cli/src/cli.module.ts Normal file
View File

@@ -0,0 +1,28 @@
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import {
StorageModule,
OutputModule,
configLoaders,
ConfigReaderModule,
getEnvFilePaths,
} from "@spaceflow/core";
@Module({
imports: [
// 配置模块
ConfigModule.forRoot({
isGlobal: true,
load: configLoaders,
envFilePath: getEnvFilePaths(),
}),
// 基础能力模块
StorageModule.forFeature(),
OutputModule,
ConfigReaderModule,
// 内置命令通过 internal-plugins.ts 以插件方式加载
],
})
export class CliModule {}

80
cli/src/cli.ts Normal file
View File

@@ -0,0 +1,80 @@
import { initI18n } from "@spaceflow/core";
// 必须在所有命令模块 import 之前初始化 i18n装饰器在 import 时执行
initI18n();
// 注册所有内部命令的 i18n 资源side-effect必须在 internal-extensions 之前
import "./locales";
import { CommandFactory } from "nest-commander";
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { OutputService, loadSpaceflowConfig } from "@spaceflow/core";
import type { ExtensionModuleType } from "@spaceflow/core";
import { ExtensionLoaderService } from "./extension-loader";
import { CliModule } from "./cli.module";
import { parseRunxArgs } from "./commands/runx/runx.utils";
import { internalExtensions } from "./internal-extensions";
/**
* 预处理 runx/x 命令的参数
* 将 source 后的所有参数转换为 -- 分隔格式,避免被 commander 解析
*/
function preprocessRunxArgs(): void {
const argv = process.argv;
// 检查是否已经有 -- 分隔符
if (argv.includes("--")) return;
const { cmdIndex, sourceIndex } = parseRunxArgs(argv);
if (cmdIndex === -1 || sourceIndex === -1) return;
// 如果 source 后还有参数,插入 -- 分隔符
if (sourceIndex + 1 < argv.length) {
process.argv = [...argv.slice(0, sourceIndex + 1), "--", ...argv.slice(sourceIndex + 1)];
}
}
async function bootstrap() {
// 预处理 runx/x 命令参数
preprocessRunxArgs();
await ConfigModule.envVariablesLoaded;
// 1. 加载 spaceflow.json 配置(运行时配置)
loadSpaceflowConfig();
// 2. 注册内部 Extension
const extensionLoader = new ExtensionLoaderService();
const internalLoaded = extensionLoader.registerInternalExtensions(internalExtensions);
// 3. 从 .spaceflow/package.json 发现并加载外部 Extension
const externalLoaded = await extensionLoader.discoverAndLoad();
// 合并所有 Extension 模块
const extensionModules: ExtensionModuleType[] = [...internalLoaded, ...externalLoaded].map(
(e) => e.module,
);
// 4. 动态创建 CLI Module
@Module({
imports: [CliModule, ...extensionModules],
})
class DynamicCliModule {}
// 4. 创建并运行 CLI
const app = await CommandFactory.createWithoutRunning(DynamicCliModule);
const output = app.get(OutputService);
await CommandFactory.runApplication(app);
// Flush outputs after command execution
output.flush();
}
bootstrap()
.then(() => {
// console.log("Bootstrap completed");
process.exit(0);
})
.catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,74 @@
import { Command, CommandRunner, Option } from "nest-commander";
import { t } from "@spaceflow/core";
import { BuildService } from "./build.service";
import type { VerboseLevel } from "@spaceflow/core";
export interface BuildOptions {
skill?: string;
watch?: boolean;
verbose?: VerboseLevel;
}
/**
* 构建命令
*
* 用于构建 skills 目录下的插件包
*
* 用法:
* spaceflow build [skill] # 构建指定或所有插件
* spaceflow build --watch # 监听模式
*/
@Command({
name: "build",
arguments: "[skill]",
description: t("build:description"),
})
export class BuildCommand extends CommandRunner {
constructor(private readonly buildService: BuildService) {
super();
}
async run(passedParams: string[], options: BuildOptions): Promise<void> {
const skill = passedParams[0];
const verbose = options.verbose ?? true;
try {
if (options.watch) {
await this.buildService.watch(skill, verbose);
} else {
const results = await this.buildService.build(skill, verbose);
const hasErrors = results.some((r) => !r.success);
if (hasErrors) {
process.exit(1);
}
}
} catch (error) {
if (error instanceof Error) {
console.error(t("build:buildFailed", { error: error.message }));
if (error.stack) {
console.error(t("common.stackTrace", { stack: error.stack }));
}
} else {
console.error(t("build:buildFailed", { error }));
}
process.exit(1);
}
}
@Option({
flags: "-w, --watch",
description: t("build:options.watch"),
})
parseWatch(val: boolean): boolean {
return val;
}
@Option({
flags: "-v, --verbose",
description: t("common.options.verbose"),
})
parseVerbose(_val: string, previous: VerboseLevel = 0): VerboseLevel {
const current = typeof previous === "number" ? previous : previous ? 1 : 0;
return Math.min(current + 1, 2) as VerboseLevel;
}
}

View File

@@ -0,0 +1,8 @@
import { Module } from "@nestjs/common";
import { BuildCommand } from "./build.command";
import { BuildService } from "./build.service";
@Module({
providers: [BuildCommand, BuildService],
})
export class BuildModule {}

View File

@@ -0,0 +1,312 @@
import { Injectable } from "@nestjs/common";
import { rspack, type Compiler, type Configuration, type Stats } from "@rspack/core";
import { readdir, stat } from "fs/promises";
import { join, dirname } from "path";
import { existsSync } from "fs";
import { createPluginConfig } from "@spaceflow/core";
import { shouldLog, type VerboseLevel, t } from "@spaceflow/core";
export interface SkillInfo {
name: string;
path: string;
hasPackageJson: boolean;
}
export interface BuildResult {
skill: string;
success: boolean;
duration?: number;
errors?: string[];
warnings?: string[];
}
@Injectable()
export class BuildService {
private readonly projectRoot = this.findProjectRoot();
private readonly skillsDir = join(this.projectRoot, "skills");
private readonly commandsDir = join(this.projectRoot, "commands");
private readonly coreRoot = join(this.projectRoot, "core");
private watchers: Map<string, Compiler> = new Map();
/**
* 查找项目根目录(包含 spaceflow.json 或 .spaceflow/spaceflow.json 的目录)
*/
private findProjectRoot(): string {
let dir = process.cwd();
while (dir !== dirname(dir)) {
// 检查根目录下的 spaceflow.json
if (existsSync(join(dir, "spaceflow.json"))) {
return dir;
}
// 检查 .spaceflow/spaceflow.json
if (existsSync(join(dir, ".spaceflow", "spaceflow.json"))) {
return dir;
}
dir = dirname(dir);
}
// 如果找不到,回退到 cwd
return process.cwd();
}
/**
* 构建插件
*/
async build(skillName?: string, verbose: VerboseLevel = 1): Promise<BuildResult[]> {
const skills = await this.getSkillsToBuild(skillName);
if (skills.length === 0) {
if (shouldLog(verbose, 1)) console.log(t("build:noPlugins"));
return [];
}
if (shouldLog(verbose, 1))
console.log(t("build:startBuilding", { count: skills.length }) + "\n");
const results: BuildResult[] = [];
for (const skill of skills) {
const result = await this.buildSkill(skill, verbose);
results.push(result);
}
const successCount = results.filter((r) => r.success).length;
const failCount = results.filter((r) => !r.success).length;
if (shouldLog(verbose, 1))
console.log("\n" + t("build:buildComplete", { success: successCount, fail: failCount }));
return results;
}
/**
* 监听模式构建
*/
async watch(skillName?: string, verbose: VerboseLevel = 1): Promise<void> {
const skills = await this.getSkillsToBuild(skillName);
if (skills.length === 0) {
if (shouldLog(verbose, 1)) console.log(t("build:noPlugins"));
return;
}
if (shouldLog(verbose, 1))
console.log(t("build:startWatching", { count: skills.length }) + "\n");
// 并行启动所有 watcher
await Promise.all(skills.map((skill) => this.watchSkill(skill, verbose)));
// 保持进程运行
await new Promise(() => {});
}
/**
* 停止所有 watcher
*/
async stopWatch(verbose: VerboseLevel = 1): Promise<void> {
for (const [name, compiler] of this.watchers) {
await new Promise<void>((resolve) => {
compiler.close(() => {
if (shouldLog(verbose, 1)) console.log(t("build:stopWatching", { name }));
resolve();
});
});
}
this.watchers.clear();
}
/**
* 获取需要构建的插件列表
*/
private async getSkillsToBuild(skillName?: string): Promise<SkillInfo[]> {
// 如果没有指定插件名,检查是否在插件目录中运行
if (!skillName) {
const currentSkill = this.detectCurrentSkill();
if (currentSkill) {
return [currentSkill];
}
}
if (!existsSync(this.skillsDir)) {
return [];
}
const entries = await readdir(this.skillsDir);
const skills: SkillInfo[] = [];
for (const entry of entries) {
if (entry.startsWith(".")) continue;
const skillPath = join(this.skillsDir, entry);
const stats = await stat(skillPath);
if (!stats.isDirectory()) continue;
if (skillName && entry !== skillName) continue;
const packageJsonPath = join(skillPath, "package.json");
const hasPackageJson = existsSync(packageJsonPath);
if (hasPackageJson) {
skills.push({
name: entry,
path: skillPath,
hasPackageJson,
});
}
}
return skills;
}
/**
* 检测当前是否在插件目录中运行
*/
private detectCurrentSkill(): SkillInfo | null {
const cwd = process.cwd();
const packageJsonPath = join(cwd, "package.json");
// 检查当前目录是否有 package.json
if (!existsSync(packageJsonPath)) {
return null;
}
// 检查是否在 skills 或 commands 目录下
if (!cwd.startsWith(this.skillsDir) && !cwd.startsWith(this.commandsDir)) {
return null;
}
// 获取插件名(当前目录名)
const name = cwd.split("/").pop() || "";
return {
name,
path: cwd,
hasPackageJson: true,
};
}
/**
* 构建单个插件
*/
private async buildSkill(skill: SkillInfo, verbose: VerboseLevel = 1): Promise<BuildResult> {
const startTime = Date.now();
if (shouldLog(verbose, 1)) console.log(t("build:building", { name: skill.name }));
try {
const config = await this.getConfig(skill);
const compiler = rspack(config);
const stats = await new Promise<Stats>((resolve, reject) => {
compiler.run((err, stats) => {
compiler.close((closeErr) => {
if (err) return reject(err);
if (closeErr) return reject(closeErr);
if (!stats) return reject(new Error("No stats returned"));
resolve(stats);
});
});
});
const duration = Date.now() - startTime;
const info = stats.toJson({ errors: true, warnings: true });
if (stats.hasErrors()) {
const errors = info.errors?.map((e) => e.message) || [];
console.log(t("build:buildFailedWithDuration", { duration }));
errors.forEach((e) => console.log(` ${e}`));
return { skill: skill.name, success: false, duration, errors };
}
if (stats.hasWarnings()) {
const warnings = info.warnings?.map((w) => w.message) || [];
if (shouldLog(verbose, 1))
console.log(t("build:buildWarnings", { duration, count: warnings.length }));
return { skill: skill.name, success: true, duration, warnings };
}
if (shouldLog(verbose, 1)) console.log(t("build:buildSuccess", { duration }));
return { skill: skill.name, success: true, duration };
} catch (error) {
const duration = Date.now() - startTime;
const message = error instanceof Error ? error.message : String(error);
console.log(t("build:buildFailedWithMessage", { duration, message }));
return { skill: skill.name, success: false, duration, errors: [message] };
}
}
/**
* 监听单个插件
*/
private async watchSkill(skill: SkillInfo, verbose: VerboseLevel = 1): Promise<void> {
if (shouldLog(verbose, 1)) console.log(t("build:watching", { name: skill.name }));
try {
const config = await this.getConfig(skill);
const compiler = rspack(config);
this.watchers.set(skill.name, compiler);
compiler.watch({}, (err, stats) => {
if (err) {
console.log(t("build:watchError", { name: skill.name, message: err.message }));
return;
}
if (!stats) return;
const info = stats.toJson({ errors: true, warnings: true });
if (stats.hasErrors()) {
console.log(t("build:watchBuildFailed", { name: skill.name }));
info.errors?.forEach((e) => console.log(` ${e.message}`));
} else if (stats.hasWarnings()) {
if (shouldLog(verbose, 1))
console.log(
t("build:watchBuildWarnings", { name: skill.name, count: info.warnings?.length }),
);
} else {
if (shouldLog(verbose, 1))
console.log(t("build:watchBuildSuccess", { name: skill.name }));
}
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.log(t("build:watchInitFailed", { name: skill.name, message }));
}
}
/**
* 获取插件的 rspack 配置
*/
private async getConfig(skill: SkillInfo): Promise<Configuration> {
// 检查是否有自定义配置
const customConfigPath = join(skill.path, "rspack.config.mjs");
const customConfigPathJs = join(skill.path, "rspack.config.js");
if (existsSync(customConfigPath)) {
const module = await import(customConfigPath);
return module.default || module;
}
if (existsSync(customConfigPathJs)) {
const module = await import(customConfigPathJs);
return module.default || module;
}
// 使用默认配置
return this.getDefaultConfig(skill);
}
/**
* 生成默认的 rspack 配置
*/
private getDefaultConfig(skill: SkillInfo): Configuration {
return createPluginConfig(
{
name: skill.name,
path: skill.path,
},
{
coreRoot: this.coreRoot,
},
);
}
}

View File

@@ -0,0 +1,28 @@
import type {
SpaceflowExtension,
SpaceflowExtensionMetadata,
ExtensionModuleType,
} from "@spaceflow/core";
import { t } from "@spaceflow/core";
import { BuildModule } from "./build.module";
export const buildMetadata: SpaceflowExtensionMetadata = {
name: "build",
commands: ["build"],
version: "1.0.0",
description: t("build:extensionDescription"),
};
export class BuildExtension implements SpaceflowExtension {
getMetadata(): SpaceflowExtensionMetadata {
return buildMetadata;
}
getModule(): ExtensionModuleType {
return BuildModule;
}
}
export * from "./build.module";
export * from "./build.command";
export * from "./build.service";

View File

@@ -0,0 +1,67 @@
import { Command, CommandRunner, Option } from "nest-commander";
import { t } from "@spaceflow/core";
import { ClearService } from "./clear.service";
import type { VerboseLevel } from "@spaceflow/core";
export interface ClearCommandOptions {
global?: boolean;
verbose?: VerboseLevel;
}
/**
* 清理所有安装的技能包
*
* 用法:
* spaceflow clear # 清理本地安装的所有 skills
* spaceflow clear -g # 清理全局安装的所有 skills
*
* 功能:
* 1. 删除 .spaceflow/deps 目录下的所有依赖
* 2. 删除各编辑器 skills 目录下的所有安装内容
* 3. 删除各编辑器 commands 目录下的所有生成的 .md 文件
*/
@Command({
name: "clear",
description: t("clear:description"),
})
export class ClearCommand extends CommandRunner {
constructor(private readonly clearService: ClearService) {
super();
}
async run(_passedParams: string[], options: ClearCommandOptions): Promise<void> {
const isGlobal = options.global ?? false;
const verbose = options.verbose ?? true;
try {
await this.clearService.execute(isGlobal, verbose);
} catch (error) {
if (error instanceof Error) {
console.error(t("clear:clearFailed", { error: error.message }));
if (error.stack) {
console.error(t("common.stackTrace", { stack: error.stack }));
}
} else {
console.error(t("clear:clearFailed", { error }));
}
process.exit(1);
}
}
@Option({
flags: "-g, --global",
description: t("clear:options.global"),
})
parseGlobal(): boolean {
return true;
}
@Option({
flags: "-v, --verbose",
description: t("common.options.verbose"),
})
parseVerbose(_val: string, previous: VerboseLevel = 0): VerboseLevel {
const current = typeof previous === "number" ? previous : previous ? 1 : 0;
return Math.min(current + 1, 2) as VerboseLevel;
}
}

View File

@@ -0,0 +1,8 @@
import { Module } from "@nestjs/common";
import { ClearCommand } from "./clear.command";
import { ClearService } from "./clear.service";
@Module({
providers: [ClearCommand, ClearService],
})
export class ClearModule {}

View File

@@ -0,0 +1,161 @@
import { Injectable } from "@nestjs/common";
import { readFile, rm } from "fs/promises";
import { join } from "path";
import { existsSync, readdirSync, lstatSync } from "fs";
import { shouldLog, type VerboseLevel, t } from "@spaceflow/core";
import { getEditorDirName, DEFAULT_EDITOR } from "@spaceflow/core";
@Injectable()
export class ClearService {
/**
* 获取支持的编辑器列表
*/
protected async getSupportedEditors(configPath: string): Promise<string[]> {
try {
if (!existsSync(configPath)) return [DEFAULT_EDITOR];
const content = await readFile(configPath, "utf-8");
const config = JSON.parse(content);
return config.support || [DEFAULT_EDITOR];
} catch {
return [DEFAULT_EDITOR];
}
}
/**
* 执行清理
*/
async execute(isGlobal = false, verbose: VerboseLevel = 1): Promise<void> {
if (shouldLog(verbose, 1)) {
if (isGlobal) {
console.log(t("clear:clearingGlobal"));
} else {
console.log(t("clear:clearing"));
}
}
const cwd = process.cwd();
const home = process.env.HOME || process.env.USERPROFILE || "~";
const configPath = join(cwd, "spaceflow.json");
// 1. 清理 .spaceflow/deps 目录
await this.clearSpaceflowDeps(isGlobal, verbose);
// 2. 清理各编辑器的 skills 和 commands 目录
const editors = await this.getSupportedEditors(configPath);
for (const editor of editors) {
const editorDirName = getEditorDirName(editor);
const editorRoot = isGlobal ? join(home, editorDirName) : join(cwd, editorDirName);
await this.clearEditorDir(editorRoot, editor, verbose);
}
if (shouldLog(verbose, 1)) console.log(t("clear:clearDone"));
}
/**
* 清理 .spaceflow 目录内部文件(保留 spaceflow.json
*/
private async clearSpaceflowDeps(isGlobal: boolean, verbose: VerboseLevel = 1): Promise<void> {
const cwd = process.cwd();
const home = process.env.HOME || process.env.USERPROFILE || "~";
const spaceflowRoot = isGlobal ? join(home, ".spaceflow") : join(cwd, ".spaceflow");
if (!existsSync(spaceflowRoot)) {
if (shouldLog(verbose, 1)) console.log(t("clear:spaceflowNotExist"));
return;
}
// 需要保留的文件
const preserveFiles = ["spaceflow.json", "package.json"];
const entries = readdirSync(spaceflowRoot);
const toDelete = entries.filter((entry) => !preserveFiles.includes(entry));
if (toDelete.length === 0) {
if (shouldLog(verbose, 1)) console.log(t("clear:spaceflowNoClean"));
return;
}
if (shouldLog(verbose, 1))
console.log(t("clear:clearingSpaceflow", { count: toDelete.length }));
for (const entry of toDelete) {
const entryPath = join(spaceflowRoot, entry);
try {
await rm(entryPath, { recursive: true, force: true });
if (shouldLog(verbose, 2)) console.log(t("clear:deleted", { entry }));
} catch (error) {
if (shouldLog(verbose, 1)) {
console.warn(
t("clear:deleteFailed", { entry }),
error instanceof Error ? error.message : error,
);
}
}
}
}
/**
* 清理编辑器目录下的 skills 和 commands
*/
private async clearEditorDir(
editorRoot: string,
editorName: string,
verbose: VerboseLevel = 1,
): Promise<void> {
if (!existsSync(editorRoot)) {
return;
}
// 清理 skills 目录
const skillsDir = join(editorRoot, "skills");
if (existsSync(skillsDir)) {
const entries = readdirSync(skillsDir);
if (entries.length > 0) {
if (shouldLog(verbose, 1))
console.log(t("clear:clearingSkills", { editor: editorName, count: entries.length }));
for (const entry of entries) {
const entryPath = join(skillsDir, entry);
try {
await rm(entryPath, { recursive: true, force: true });
if (shouldLog(verbose, 2)) console.log(t("clear:deleted", { entry }));
} catch (error) {
if (shouldLog(verbose, 1)) {
console.warn(
t("clear:deleteFailed", { entry }),
error instanceof Error ? error.message : error,
);
}
}
}
}
}
// 清理 commands 目录中的 .md 文件
const commandsDir = join(editorRoot, "commands");
if (existsSync(commandsDir)) {
const entries = readdirSync(commandsDir).filter((f) => f.endsWith(".md"));
if (entries.length > 0) {
if (shouldLog(verbose, 1))
console.log(t("clear:clearingCommands", { editor: editorName, count: entries.length }));
for (const entry of entries) {
const entryPath = join(commandsDir, entry);
try {
const stats = lstatSync(entryPath);
if (stats.isFile()) {
await rm(entryPath);
if (shouldLog(verbose, 2)) console.log(t("clear:deleted", { entry }));
}
} catch (error) {
if (shouldLog(verbose, 1)) {
console.warn(
t("clear:deleteFailed", { entry }),
error instanceof Error ? error.message : error,
);
}
}
}
}
}
}
}

View File

@@ -0,0 +1,28 @@
import type {
SpaceflowExtension,
SpaceflowExtensionMetadata,
ExtensionModuleType,
} from "@spaceflow/core";
import { t } from "@spaceflow/core";
import { ClearModule } from "./clear.module";
export const clearMetadata: SpaceflowExtensionMetadata = {
name: "clear",
commands: ["clear"],
version: "1.0.0",
description: t("clear:extensionDescription"),
};
export class ClearExtension implements SpaceflowExtension {
getMetadata(): SpaceflowExtensionMetadata {
return clearMetadata;
}
getModule(): ExtensionModuleType {
return ClearModule;
}
}
export * from "./clear.command";
export * from "./clear.service";
export * from "./clear.module";

View File

@@ -0,0 +1,108 @@
import { Command, CommandRunner, Option } from "nest-commander";
import { t } from "@spaceflow/core";
import { CommitService } from "./commit.service";
import type { VerboseLevel } from "@spaceflow/core";
export interface CommitCommandOptions {
verbose?: VerboseLevel;
dryRun?: boolean;
noVerify?: boolean;
split?: boolean;
}
/**
* Commit 命令
*
* 基于暂存区代码变更和历史 commit 自动生成规范的 commit message
*
* 用法:
* spaceflow commit # 生成并提交
* spaceflow commit --dry-run # 仅生成,不提交
* spaceflow commit --verbose # 显示详细信息
*/
@Command({
name: "commit",
description: t("commit:description"),
})
export class CommitCommand extends CommandRunner {
constructor(private readonly commitService: CommitService) {
super();
}
async run(_passedParams: string[], options: CommitCommandOptions): Promise<void> {
const verbose = options.verbose ?? 0;
try {
const result = await this.commitService.generateAndCommit({
verbose,
dryRun: options.dryRun,
noVerify: options.noVerify,
split: options.split,
});
if (result.success) {
if (options.dryRun) {
console.log(t("commit:dryRunMode"));
console.log(result.message);
} else {
if (result.commitCount && result.commitCount > 1) {
console.log(t("commit:splitSuccess", { count: result.commitCount }));
// 分批提交时已经实时打印了每个 commit不再重复打印
} else {
console.log(t("commit:commitSuccess"));
if (result.message) {
console.log(result.message);
}
}
}
} else {
console.error(t("commit:commitFailed", { error: result.error }));
process.exit(1);
}
} catch (error) {
if (error instanceof Error) {
console.error(t("common.executionFailed", { error: error.message }));
if (error.stack) {
console.error(t("common.stackTrace", { stack: error.stack }));
}
} else {
console.error(t("common.executionFailed", { error }));
}
process.exit(1);
}
}
@Option({
flags: "-d, --dry-run",
description: t("commit:options.dryRun"),
})
parseDryRun(): boolean {
return true;
}
@Option({
flags: "-v, --verbose",
description: t("common.options.verbose"),
})
parseVerbose(_val: string, previous: VerboseLevel = 0): VerboseLevel {
// 每次 -v 增加一级,最高为 2
const current = typeof previous === "number" ? previous : previous ? 1 : 0;
return Math.min(current + 1, 2) as VerboseLevel;
}
@Option({
flags: "-n, --no-verify",
description: t("commit:options.noVerify"),
})
parseNoVerify(): boolean {
return true;
}
@Option({
flags: "-s, --split",
description: t("commit:options.split"),
})
parseSplit(): boolean {
return true;
}
}

View File

@@ -0,0 +1,168 @@
import { z } from "zod";
import type { VerboseLevel } from "@spaceflow/core";
/**
* Commit 类型定义
*/
export interface CommitType {
type: string;
section: string;
}
/**
* Changelog 配置
*/
export interface CommitConfig {
changelog?: {
preset?: {
type?: CommitType[];
};
};
}
/**
* Scope 匹配规则 schema
*/
export const ScopeRuleSchema = z.object({
/** glob 模式,如 "src/components/**", "docs/**" */
pattern: z.string().describe("glob 模式,用于匹配文件路径"),
/** 匹配后使用的 scope 名称 */
scope: z.string().describe("匹配后使用的 scope 名称"),
});
export type ScopeRule = z.infer<typeof ScopeRuleSchema>;
/**
* Commit scope 配置 schema
*/
export const CommitScopeConfigSchema = z.object({
/**
* 分组策略
* - "package": 按最近的 package.json 目录分组(默认)
* - "rules": 仅使用自定义规则
* - "rules-first": 优先使用自定义规则,未匹配则回退到 package 策略
*/
strategy: z.enum(["package", "rules", "rules-first"]).default("package").describe("文件分组策略"),
/** 自定义匹配规则列表 */
rules: z.array(ScopeRuleSchema).default([]).describe("自定义 scope 匹配规则"),
});
export type CommitScopeConfig = z.infer<typeof CommitScopeConfigSchema>;
/**
* Commit 命令选项
*/
export interface CommitOptions {
verbose?: VerboseLevel;
dryRun?: boolean;
noVerify?: boolean;
split?: boolean;
}
/**
* Commit 执行结果
*/
export interface CommitResult {
success: boolean;
message?: string;
error?: string;
commitCount?: number;
}
/**
* Commit 分组
*/
export interface CommitGroup {
files: string[];
reason: string;
packageInfo?: { name: string; description?: string };
}
/**
* 拆分分析结果
*/
export interface SplitAnalysis {
groups: CommitGroup[];
}
/**
* 包信息
*/
export interface PackageInfo {
/** 包名package.json 中的 name */
name: string;
/** 包描述 */
description?: string;
/** package.json 所在目录的绝对路径 */
path: string;
}
/**
* 结构化 Commit Message schema仅 AI 生成的部分)
*/
export const CommitMessageContentSchema = z.object({
/** commit 类型,如 feat, fix, refactor 等 */
type: z.string().describe("commit 类型"),
/** 影响范围,可选 */
scope: z.string().optional().describe("影响范围(包名、模块名)"),
/** 简短描述,不超过 50 个字符 */
subject: z.string().describe("简短描述"),
/** 详细描述,可选 */
body: z.string().optional().describe("详细描述"),
});
export type CommitMessageContent = z.infer<typeof CommitMessageContentSchema>;
/**
* 完整的 Commit Message包含文件和包上下文
*/
export interface CommitMessage extends CommitMessageContent {
/** 涉及的文件列表(相对路径) */
files?: string[];
/** 所属包信息 */
packageInfo?: PackageInfo;
}
/**
* 解析 commit message 字符串为结构化对象
*/
export function parseCommitMessage(message: string): CommitMessage {
const lines = message.trim().split("\n");
const firstLine = lines[0] || "";
// 匹配 type(scope): subject 或 type: subject
const headerRegex = /^(\w+)(?:\(([^)]+)\))?:\s*(.+)$/;
const match = firstLine.match(headerRegex);
if (!match) {
// 无法解析,将整个内容作为 subject
return {
type: "chore",
subject: firstLine || message.trim(),
body: lines.slice(1).join("\n").trim() || undefined,
};
}
const [, type, scope, subject] = match;
const body = lines.slice(1).join("\n").trim() || undefined;
return {
type,
scope: scope || undefined,
subject,
body,
};
}
/**
* 格式化结构化 commit message 为字符串
*/
export function formatCommitMessage(commit: CommitMessage): string {
const { type, scope, subject, body } = commit;
const header = scope ? `${type}(${scope}): ${subject}` : `${type}: ${subject}`;
if (body) {
return `${header}\n\n${body}`;
}
return header;
}

View File

@@ -0,0 +1,25 @@
import { Module } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { CommitCommand } from "./commit.command";
import { CommitService } from "./commit.service";
import { ConfigReaderModule, LlmProxyModule, type LlmConfig } from "@spaceflow/core";
@Module({
imports: [
ConfigReaderModule,
LlmProxyModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
const llmConfig = configService.get<LlmConfig>("llm");
return {
defaultAdapter: "openai",
openai: llmConfig?.openai,
claudeCode: llmConfig?.claudeCode,
openCode: llmConfig?.openCode,
};
},
}),
],
providers: [CommitCommand, CommitService],
})
export class CommitModule {}

View File

@@ -0,0 +1,952 @@
import { Injectable } from "@nestjs/common";
import { ConfigReaderService } from "@spaceflow/core";
import { execSync, spawnSync } from "child_process";
import { existsSync, readFileSync } from "fs";
import micromatch from "micromatch";
import { dirname, join } from "path";
import {
LlmProxyService,
type LlmMessage,
LlmJsonPut,
parallel,
shouldLog,
t,
} from "@spaceflow/core";
import {
CommitScopeConfigSchema,
formatCommitMessage,
parseCommitMessage,
type CommitConfig,
type CommitGroup,
type CommitMessage,
type CommitOptions,
type CommitResult,
type CommitScopeConfig,
type CommitType,
type PackageInfo,
type ScopeRule,
type SplitAnalysis,
} from "./commit.config";
// 重新导出类型,保持向后兼容
export type {
CommitConfig,
CommitGroup,
CommitMessage,
CommitOptions,
CommitResult,
CommitScopeConfig,
CommitType,
PackageInfo,
ScopeRule,
SplitAnalysis,
};
export { formatCommitMessage, parseCommitMessage };
/**
* Commit 上下文,包含生成 commit message 所需的所有信息
*/
interface CommitContext {
files: string[];
diff: string;
scope: string;
packageInfo?: PackageInfo;
}
@Injectable()
export class CommitService {
constructor(
private readonly configReader: ConfigReaderService,
private readonly llmProxyService: LlmProxyService,
) {}
// ============================================================
// Git 基础操作
// ============================================================
/**
* 执行 git 命令并返回输出
*/
private execGit(command: string, options?: { maxBuffer?: number }): string {
return execSync(command, {
encoding: "utf-8",
maxBuffer: options?.maxBuffer ?? 1024 * 1024 * 10,
stdio: ["pipe", "pipe", "pipe"],
}).trim();
}
/**
* 安全执行 git 命令,失败返回空字符串
*/
private execGitSafe(command: string, options?: { maxBuffer?: number }): string {
try {
return this.execGit(command, options);
} catch {
return "";
}
}
/**
* 获取文件列表(从 git 命令输出解析)
*/
private parseFileList(output: string): string[] {
return output
.split("\n")
.map((f) => f.trim())
.filter((f) => f.length > 0);
}
// ============================================================
// 暂存区操作
// ============================================================
getStagedFiles(): string[] {
return this.parseFileList(this.execGitSafe("git diff --cached --name-only"));
}
getStagedDiff(): string {
try {
return this.execGit("git diff --cached --no-color");
} catch {
throw new Error(t("commit:getDiffFailed"));
}
}
hasStagedFiles(): boolean {
return this.getStagedFiles().length > 0;
}
getFileDiff(files: string[]): string {
if (files.length === 0) return "";
const fileArgs = files.map((f) => `"${f}"`).join(" ");
return this.execGitSafe(`git diff --cached --no-color -- ${fileArgs}`);
}
// ============================================================
// 工作区操作
// ============================================================
getUnstagedFiles(): string[] {
return this.parseFileList(this.execGitSafe("git diff --name-only"));
}
getUntrackedFiles(): string[] {
return this.parseFileList(this.execGitSafe("git ls-files --others --exclude-standard"));
}
getAllWorkingFiles(): string[] {
return [...new Set([...this.getUnstagedFiles(), ...this.getUntrackedFiles()])];
}
hasWorkingFiles(): boolean {
return this.getAllWorkingFiles().length > 0;
}
getUnstagedFileDiff(files: string[]): string {
if (files.length === 0) return "";
const fileArgs = files.map((f) => `"${f}"`).join(" ");
return this.execGitSafe(`git diff --no-color -- ${fileArgs}`);
}
// ============================================================
// 历史记录
// ============================================================
getRecentCommits(count: number = 10): string {
return this.execGitSafe(`git log --oneline -n ${count} --no-color`);
}
// ============================================================
// 配置获取
// ============================================================
getCommitTypes(): CommitType[] {
const publishConfig = this.configReader.getPluginConfig<CommitConfig>("publish");
const defaultTypes: CommitType[] = [
{ type: "feat", section: "新特性" },
{ type: "fix", section: "修复BUG" },
{ type: "perf", section: "性能优化" },
{ type: "refactor", section: "代码重构" },
{ type: "docs", section: "文档更新" },
{ type: "style", section: "代码格式" },
{ type: "test", section: "测试用例" },
{ type: "chore", section: "其他修改" },
];
return publishConfig?.changelog?.preset?.type || defaultTypes;
}
getScopeConfig(): CommitScopeConfig {
const commitConfig = this.configReader.getPluginConfig<Record<string, unknown>>("commit");
return CommitScopeConfigSchema.parse(commitConfig ?? {});
}
// ============================================================
// Package.json 和 Scope 处理
// ============================================================
/**
* 从路径提取 scope目录名
* 根目录返回空字符串
*/
extractScopeFromPath(packagePath: string): string {
if (!packagePath || packagePath === process.cwd()) return "";
const parts = packagePath.split("/").filter(Boolean);
return parts[parts.length - 1] || "";
}
/**
* 查找文件所属的 package.json
*/
findPackageForFile(file: string): PackageInfo {
const cwd = process.cwd();
let dir = dirname(join(cwd, file));
while (dir !== dirname(dir)) {
const pkgPath = join(dir, "package.json");
if (existsSync(pkgPath)) {
try {
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
return {
name: pkg.name || "root",
description: pkg.description,
path: dir,
};
} catch {
// 解析失败,继续向上
}
}
dir = dirname(dir);
}
// 回退到根目录
return this.getRootPackageInfo();
}
/**
* 获取根目录的 package.json 信息
*/
private getRootPackageInfo(): PackageInfo {
const cwd = process.cwd();
const pkgPath = join(cwd, "package.json");
if (existsSync(pkgPath)) {
try {
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
return { name: pkg.name || "root", description: pkg.description, path: cwd };
} catch {
// ignore
}
}
return { name: "root", path: cwd };
}
/**
* 按 package.json 对文件分组
*/
groupFilesByPackage(files: string[]): Map<string, { files: string[]; packageInfo: PackageInfo }> {
const groups = new Map<string, { files: string[]; packageInfo: PackageInfo }>();
for (const file of files) {
const pkgInfo = this.findPackageForFile(file);
const key = pkgInfo.path;
if (!groups.has(key)) {
groups.set(key, { files: [], packageInfo: pkgInfo });
}
groups.get(key)!.files.push(file);
}
return groups;
}
/**
* 根据自定义规则匹配文件的 scope
*/
matchFileToScope(file: string, rules: ScopeRule[]): string | null {
for (const rule of rules) {
if (micromatch.isMatch(file, rule.pattern)) {
return rule.scope;
}
}
return null;
}
/**
* 根据配置策略对文件分组
*/
groupFiles(
files: string[],
): Map<string, { files: string[]; scope: string; packageInfo?: PackageInfo }> {
const config = this.getScopeConfig();
const groups = new Map<string, { files: string[]; scope: string; packageInfo?: PackageInfo }>();
for (const file of files) {
let scope: string | null = null;
let packageInfo: PackageInfo | undefined;
// 规则匹配
if (config.strategy === "rules" || config.strategy === "rules-first") {
scope = this.matchFileToScope(file, config.rules || []);
}
// 包目录匹配
if (scope === null && (config.strategy === "package" || config.strategy === "rules-first")) {
packageInfo = this.findPackageForFile(file);
scope = this.extractScopeFromPath(packageInfo.path);
}
const finalScope = scope || "";
if (!groups.has(finalScope)) {
groups.set(finalScope, { files: [], scope: finalScope, packageInfo });
}
groups.get(finalScope)!.files.push(file);
}
return groups;
}
// ============================================================
// Commit 上下文获取
// ============================================================
/**
* 获取 commit 上下文(统一获取 files, diff, scope, packageInfo
*/
getCommitContext(files: string[], useUnstaged = false): CommitContext {
const diff = useUnstaged ? this.getUnstagedFileDiff(files) : this.getFileDiff(files);
const packageGroups = this.groupFilesByPackage(files);
const firstGroup = [...packageGroups.values()][0];
const packageInfo = firstGroup?.packageInfo;
const scope = packageInfo ? this.extractScopeFromPath(packageInfo.path) : "";
return { files, diff, scope, packageInfo };
}
// ============================================================
// Prompt 构建
// ============================================================
/**
* 构建 commit message 生成的 prompt
*/
private buildCommitPrompt(ctx: CommitContext): { system: string; user: string } {
const commitTypes = this.getCommitTypes();
const typesList = commitTypes.map((t) => `- ${t.type}: ${t.section}`).join("\n");
const recentCommits = this.getRecentCommits();
const packageContext = ctx.packageInfo
? `\n## 包信息\n- 包名: ${ctx.packageInfo.name}\n- scope: ${ctx.scope || "无(根目录)"}${ctx.packageInfo.description ? `\n- 描述: ${ctx.packageInfo.description}` : ""}`
: "";
const system = `你是一个专业的 Git commit message 生成器。请根据提供的代码变更生成符合 Conventional Commits 规范的 commit message。
## Commit 类型规范
${typesList}
## 输出格式
请严格按照以下 JSON 格式输出,不要包含任何其他内容:
{
"type": "feat",
"scope": "${ctx.scope}",
"subject": "简短描述不超过50字符中文",
"body": "详细描述(可选,中文)"
}
## 规则
1. type 必须是上述类型之一
2. scope ${ctx.scope ? `必须使用 "${ctx.scope}"` : "必须为空字符串(根目录不需要 scope"}
3. subject 是简短描述,不超过 50 个字符,使用中文
4. body 是详细描述,可选,使用中文,如果没有则设为空字符串
5. 如果变更涉及多个方面,选择最主要的类型`;
const truncatedDiff =
ctx.diff.length > 8000 ? ctx.diff.substring(0, 8000) + "\n... (diff 过长,已截断)" : ctx.diff;
const user = `请根据以下信息生成 commit message
${packageContext}
## 暂存的文件
${ctx.files.join("\n")}
## 最近的 commit 历史(参考风格)
${recentCommits || "无历史记录"}
## 代码变更 (diff)
\`\`\`diff
${truncatedDiff}
\`\`\`
请直接输出 JSON 格式的 commit message。`;
return { system, user };
}
// ============================================================
// AI 响应解析
// ============================================================
/**
* 解析 AI 响应为结构化 CommitMessage
*/
private async parseAIResponse(content: string, expectedScope: string): Promise<CommitMessage> {
const jsonPut = new LlmJsonPut<{
type: string;
scope?: string;
subject: string;
body?: string;
}>({
type: "object",
properties: {
type: { type: "string", description: "commit 类型" },
scope: { type: "string", description: "影响范围" },
subject: { type: "string", description: "简短描述" },
body: { type: "string", description: "详细描述" },
},
required: ["type", "subject"],
});
try {
const parsed = await jsonPut.parse(content, { disableRequestRetry: true });
return this.normalizeScope(
{
type: parsed.type || "chore",
subject: parsed.subject || "",
scope: parsed.scope || undefined,
body: parsed.body || undefined,
},
expectedScope,
);
} catch {
return this.normalizeScope(parseCommitMessage(content), expectedScope);
}
}
/**
* 规范化 scope强制使用预期值或移除
*/
private normalizeScope(commit: CommitMessage, expectedScope: string): CommitMessage {
return expectedScope ? { ...commit, scope: expectedScope } : { ...commit, scope: undefined };
}
// ============================================================
// Commit Message 生成
// ============================================================
/**
* 生成 commit message核心方法
*/
async generateCommitMessage(
options?: CommitOptions & { files?: string[]; useUnstaged?: boolean },
): Promise<CommitMessage> {
// 获取文件列表
const files = options?.files ?? this.getStagedFiles();
if (files.length === 0) {
throw new Error(t("commit:noFilesToCommit"));
}
// 获取上下文
const ctx = this.getCommitContext(files, options?.useUnstaged);
if (!ctx.diff) {
throw new Error(t("commit:noChanges"));
}
// 构建 prompt
const prompt = this.buildCommitPrompt(ctx);
const messages: LlmMessage[] = [
{ role: "system", content: prompt.system },
{ role: "user", content: prompt.user },
];
if (shouldLog(options?.verbose, 1)) {
console.log(t("commit:generatingMessage"));
}
// 调用 AI
const response = await this.llmProxyService.chat(messages, {
verbose: options?.verbose,
});
// 解析响应并填充上下文
const commit = await this.parseAIResponse(response.content, ctx.scope);
return {
...commit,
files: ctx.files,
packageInfo: ctx.packageInfo,
};
}
/**
* 为指定文件生成 commit message兼容旧 API
*/
async generateCommitMessageForFiles(
files: string[],
options?: CommitOptions,
useUnstaged = false,
_packageInfo?: { name: string; description?: string },
): Promise<CommitMessage> {
return this.generateCommitMessage({ ...options, files, useUnstaged });
}
// ============================================================
// 拆分分析
// ============================================================
/**
* 分析如何拆分 commit
*/
async analyzeSplitStrategy(options?: CommitOptions, useUnstaged = false): Promise<SplitAnalysis> {
const files = useUnstaged ? this.getUnstagedFiles() : this.getStagedFiles();
const config = this.getScopeConfig();
if (shouldLog(options?.verbose, 1)) {
const strategyName =
config.strategy === "rules"
? t("commit:strategyRules")
: config.strategy === "rules-first"
? t("commit:strategyRulesFirst")
: t("commit:strategyPackage");
console.log(t("commit:groupingByStrategy", { strategy: strategyName }));
}
const scopeGroups = this.groupFiles(files);
// 单个组:让 AI 进一步分析
if (scopeGroups.size === 1) {
const [, groupData] = [...scopeGroups.entries()][0];
const packageInfo = groupData.packageInfo || {
name: groupData.scope || "root",
path: process.cwd(),
};
return this.analyzeWithinPackage(groupData.files, packageInfo, options, useUnstaged);
}
// 多个组:每个组作为独立 commit
if (shouldLog(options?.verbose, 1)) {
console.log(t("commit:detectedGroups", { count: scopeGroups.size }));
}
const groups: CommitGroup[] = [];
for (const [, groupData] of scopeGroups) {
groups.push({
files: groupData.files,
reason: groupData.scope
? t("commit:scopeChanges", { scope: groupData.scope })
: t("commit:rootChanges"),
packageInfo: groupData.packageInfo
? { name: groupData.packageInfo.name, description: groupData.packageInfo.description }
: undefined,
});
}
return { groups };
}
/**
* 在单个包内分析拆分策略
*/
private async analyzeWithinPackage(
files: string[],
packageInfo: PackageInfo,
options?: CommitOptions,
useUnstaged = false,
): Promise<SplitAnalysis> {
const diff = useUnstaged ? this.getUnstagedFileDiff(files) : this.getFileDiff(files);
const scope = this.extractScopeFromPath(packageInfo.path);
const commitTypes = this.getCommitTypes();
const typesList = commitTypes.map((t) => `- ${t.type}: ${t.section}`).join("\n");
const systemPrompt = `你是一个专业的 Git commit 拆分分析器。请根据暂存的文件和代码变更,分析如何将这些变更拆分为多个独立的 commit。
## 当前包信息
- 包名: ${packageInfo.name}
- scope: ${scope || "无"}
${packageInfo.description ? `- 描述: ${packageInfo.description}` : ""}
## Commit 类型规范
${typesList}
## 拆分原则
1. **按逻辑拆分**:相关的功能变更放在一起
2. **按业务拆分**:不同业务模块的变更分开
3. **保持原子性**:每个 commit 是完整的、可独立理解的变更
4. **最小化拆分**:如果变更本身是整体,不要强行拆分
## 输出格式
请严格按照以下 JSON 格式输出:
{
"groups": [
{ "files": ["file1.ts", "file2.ts"], "reason": "简短描述" }
]
}`;
const truncatedDiff =
diff.length > 12000 ? diff.substring(0, 12000) + "\n... (diff 过长,已截断)" : diff;
const userPrompt = `请分析以下改动文件,决定如何拆分 commit
## 改动的文件
${files.join("\n")}
## 代码变更 (diff)
\`\`\`diff
${truncatedDiff}
\`\`\`
请输出 JSON 格式的拆分策略。`;
if (shouldLog(options?.verbose, 1)) {
console.log(t("commit:analyzingSplit"));
}
const response = await this.llmProxyService.chat(
[
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
{ verbose: options?.verbose },
);
return this.parseSplitAnalysis(response.content, files, packageInfo);
}
/**
* 解析拆分分析结果
*/
private parseSplitAnalysis(
content: string,
files: string[],
packageInfo: PackageInfo,
): SplitAnalysis {
let text = content
.trim()
.replace(/^```[\w]*\n?/, "")
.replace(/\n?```$/, "")
.trim();
try {
const analysis = JSON.parse(text) as SplitAnalysis;
if (!analysis.groups || !Array.isArray(analysis.groups)) {
throw new Error("Invalid response");
}
const validFiles = new Set(files);
// 过滤无效文件,移除空组
for (const group of analysis.groups) {
group.files = (group.files || []).filter((f) => validFiles.has(f));
}
analysis.groups = analysis.groups.filter((g) => g.files.length > 0);
// 补充未分配的文件
const assignedFiles = new Set(analysis.groups.flatMap((g) => g.files));
const missingFiles = files.filter((f) => !assignedFiles.has(f));
if (missingFiles.length > 0) {
if (analysis.groups.length > 0) {
analysis.groups[analysis.groups.length - 1].files.push(...missingFiles);
} else {
analysis.groups.push({ files: missingFiles, reason: t("commit:otherChanges") });
}
}
// 添加 packageInfo
for (const group of analysis.groups) {
group.packageInfo = { name: packageInfo.name, description: packageInfo.description };
}
return analysis;
} catch {
return {
groups: [
{
files,
reason: t("commit:allChanges"),
packageInfo: { name: packageInfo.name, description: packageInfo.description },
},
],
};
}
}
// ============================================================
// Commit 执行
// ============================================================
/**
* 执行 git commit
*/
async commit(message: string, options?: CommitOptions): Promise<CommitResult> {
if (options?.dryRun) {
return { success: true, message: t("commit:dryRunMessage", { message }) };
}
try {
const args = ["commit", "-m", message];
if (options?.noVerify) args.push("--no-verify");
const result = spawnSync("git", args, { encoding: "utf-8", stdio: "pipe" });
if (result.status !== 0) {
return { success: false, error: result.stderr || result.stdout || t("commit:commitFail") };
}
return { success: true, message: result.stdout };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
/**
* 暂存文件
*/
private stageFiles(files: string[]): { success: boolean; error?: string } {
const result = spawnSync("git", ["add", ...files], { encoding: "utf-8", stdio: "pipe" });
if (result.status !== 0) {
return { success: false, error: t("commit:stageFilesFailed", { error: result.stderr }) };
}
return { success: true };
}
/**
* 重置暂存区
*/
private resetStaging(): boolean {
try {
execSync("git reset HEAD", { encoding: "utf-8", stdio: "pipe" });
return true;
} catch {
return false;
}
}
// ============================================================
// 批量提交
// ============================================================
/**
* 排序 groups子包优先根目录最后
*/
private sortGroupsForCommit(groups: CommitGroup[]): CommitGroup[] {
return [...groups].sort((a, b) => {
const aScope = this.getGroupScope(a);
const bScope = this.getGroupScope(b);
if (!aScope && bScope) return 1;
if (aScope && !bScope) return -1;
return 0;
});
}
private getGroupScope(group: CommitGroup): string {
if (!group.packageInfo) return "";
const pkgGroups = this.groupFilesByPackage(group.files);
const first = [...pkgGroups.values()][0];
return first ? this.extractScopeFromPath(first.packageInfo.path) : "";
}
/**
* 分批提交
*/
async commitInBatches(options?: CommitOptions, useWorking = false): Promise<CommitResult> {
const files = useWorking ? this.getAllWorkingFiles() : this.getStagedFiles();
if (files.length === 0) {
return {
success: false,
error: useWorking ? t("commit:noWorkingChanges") : t("commit:noStagedFiles"),
};
}
const analysis = await this.analyzeSplitStrategy(options, useWorking);
const sortedGroups = this.sortGroupsForCommit(analysis.groups);
// 单个组:简化处理
if (sortedGroups.length <= 1) {
const group = sortedGroups[0];
if (shouldLog(options?.verbose, 1)) {
console.log(t("commit:singleCommit"));
}
if (useWorking) {
const stageResult = this.stageFiles(files);
if (!stageResult.success) return { success: false, error: stageResult.error };
}
const commitObj = await this.generateCommitMessage({ ...options, files });
const message = formatCommitMessage(commitObj);
if (shouldLog(options?.verbose, 1)) {
console.log(t("commit:generatedMessage"));
console.log("─".repeat(50));
console.log(message);
console.log("─".repeat(50));
}
return this.commit(message, options);
}
// 多个组:并行生成 message顺序提交
if (shouldLog(options?.verbose, 1)) {
console.log(t("commit:splitIntoCommits", { count: sortedGroups.length }));
sortedGroups.forEach((g, i) => {
const pkgStr = g.packageInfo ? ` [${g.packageInfo.name}]` : "";
console.log(
t("commit:groupItem", {
index: i + 1,
reason: g.reason,
pkg: pkgStr,
count: g.files.length,
}),
);
});
console.log(t("commit:parallelGenerating", { count: sortedGroups.length }));
}
// 并行生成 messages
const executor = parallel({ concurrency: 5 });
const messageResults = await executor.map(
sortedGroups,
async (group: CommitGroup) =>
this.generateCommitMessage({ ...options, files: group.files, useUnstaged: useWorking }),
(group: CommitGroup) => group.files[0] || "unknown",
);
const failedResults = messageResults.filter((r) => !r.success);
if (failedResults.length > 0) {
return {
success: false,
error: t("commit:generateMessageFailed", {
errors: failedResults.map((r) => r.error?.message).join(", "),
}),
};
}
const generatedMessages = messageResults.map((r) => r.result!);
if (shouldLog(options?.verbose, 1)) {
console.log(t("commit:allMessagesGenerated"));
}
// Dry run 模式
if (options?.dryRun) {
const preview = sortedGroups
.map((g, i) => {
const pkgStr = g.packageInfo ? `\n包: ${g.packageInfo.name}` : "";
return `[Commit ${i + 1}/${sortedGroups.length}] ${g.reason}${pkgStr}\n文件: ${g.files.join(", ")}\n\n${formatCommitMessage(generatedMessages[i])}`;
})
.join("\n\n" + "═".repeat(50) + "\n\n");
return { success: true, message: preview, commitCount: sortedGroups.length };
}
// 实际提交
if (this.hasStagedFiles() && !this.resetStaging()) {
return { success: false, error: t("commit:resetStagingFailed") };
}
const committedMessages: string[] = [];
let successCount = 0;
for (let i = 0; i < sortedGroups.length; i++) {
const group = sortedGroups[i];
const commitObj = generatedMessages[i];
const messageStr = formatCommitMessage(commitObj);
if (shouldLog(options?.verbose, 1)) {
const pkgStr = group.packageInfo ? ` [${group.packageInfo.name}]` : "";
console.log(
t("commit:committingGroup", {
current: i + 1,
total: sortedGroups.length,
reason: group.reason,
pkg: pkgStr,
}),
);
}
try {
const stageResult = this.stageFiles(group.files);
if (!stageResult.success) throw new Error(stageResult.error);
const diff = this.getFileDiff(group.files);
if (!diff) {
if (shouldLog(options?.verbose, 1)) console.log(t("commit:skippingNoChanges"));
continue;
}
if (shouldLog(options?.verbose, 1)) {
console.log(t("commit:commitMessage"));
console.log("─".repeat(50));
console.log(messageStr);
console.log("─".repeat(50));
}
const commitResult = await this.commit(messageStr, options);
if (!commitResult.success) throw new Error(commitResult.error);
successCount++;
const shortMsg = formatCommitMessage({ ...commitObj, body: undefined });
committedMessages.push(`✅ Commit ${i + 1}: ${shortMsg}`);
console.log(committedMessages[committedMessages.length - 1]);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
committedMessages.push(t("commit:commitItemFailed", { index: i + 1, error: errorMsg }));
// 恢复剩余文件
const remaining = sortedGroups.slice(i).flatMap((g) => g.files);
if (remaining.length > 0) this.stageFiles(remaining);
return {
success: false,
error: t("commit:commitItemFailedDetail", {
index: i + 1,
error: errorMsg,
committed: committedMessages.join("\n"),
}),
commitCount: successCount,
};
}
}
return { success: true, message: committedMessages.join("\n"), commitCount: successCount };
}
// ============================================================
// 主入口
// ============================================================
/**
* 生成并提交(主入口)
*/
async generateAndCommit(options?: CommitOptions): Promise<CommitResult> {
// Split 模式
if (options?.split) {
const hasWorking = this.hasWorkingFiles();
const hasStaged = this.hasStagedFiles();
if (!hasWorking && !hasStaged) {
return { success: false, error: t("commit:noChangesAll") };
}
return this.commitInBatches(options, hasWorking);
}
// 普通模式
if (!this.hasStagedFiles()) {
return { success: false, error: t("commit:noStagedFilesHint") };
}
try {
const commitObj = await this.generateCommitMessage(options);
const message = formatCommitMessage(commitObj);
if (shouldLog(options?.verbose, 1)) {
console.log(t("commit:generatedMessage"));
console.log("─".repeat(50));
console.log(message);
console.log("─".repeat(50));
}
return this.commit(message, options);
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
}

View File

@@ -0,0 +1,28 @@
import type {
SpaceflowExtension,
SpaceflowExtensionMetadata,
ExtensionModuleType,
} from "@spaceflow/core";
import { t } from "@spaceflow/core";
import { CommitScopeConfigSchema } from "./commit.config";
import { CommitModule } from "./commit.module";
/** commit 插件元数据 */
export const commitMetadata: SpaceflowExtensionMetadata = {
name: "commit",
commands: ["commit"],
configKey: "commit",
configSchema: () => CommitScopeConfigSchema,
version: "1.0.0",
description: t("commit:extensionDescription"),
};
export class CommitExtension implements SpaceflowExtension {
getMetadata(): SpaceflowExtensionMetadata {
return commitMetadata;
}
getModule(): ExtensionModuleType {
return CommitModule;
}
}

View File

@@ -0,0 +1,150 @@
import { Command, CommandRunner, Option } from "nest-commander";
import { t } from "@spaceflow/core";
import { CreateService } from "./create.service";
import type { VerboseLevel } from "@spaceflow/core";
export interface CreateOptions {
directory?: string;
list?: boolean;
from?: string;
ref?: string;
verbose?: VerboseLevel;
}
/**
* 创建插件命令
*
* 用法:
* spaceflow create <template> <name> [--directory <dir>]
* spaceflow create --from <repo> <template> <name> 使用远程模板仓库
* spaceflow create --list 查看可用模板
*
* 模板类型动态读取自 templates/ 目录或远程仓库
*/
@Command({
name: "create",
arguments: "[template] [name]",
description: t("create:description"),
})
export class CreateCommand extends CommandRunner {
constructor(protected readonly createService: CreateService) {
super();
}
async run(passedParams: string[], options: CreateOptions): Promise<void> {
const verbose = options.verbose ?? true;
// 列出可用模板
if (options.list) {
await this.listTemplates(options, verbose);
return;
}
const template = passedParams[0];
const name = passedParams[1];
// 无参数时显示帮助
if (!template) {
await this.showHelp();
return;
}
if (!name) {
console.error(t("create:noName"));
console.error(t("create:usage"));
process.exit(1);
}
try {
// 如果指定了远程仓库,先获取模板
if (options.from) {
await this.createService.ensureRemoteTemplates(options.from, options.ref, verbose);
}
await this.createService.createFromTemplate(template, name, options, verbose);
} catch (error) {
if (error instanceof Error) {
console.error(t("create:createFailed", { error: error.message }));
if (error.stack) {
console.error(t("common.stackTrace", { stack: error.stack }));
}
} else {
console.error(t("create:createFailed", { error }));
}
process.exit(1);
}
}
@Option({
flags: "-d, --directory <dir>",
description: t("create:options.directory"),
})
parseDirectory(val: string): string {
return val;
}
@Option({
flags: "-l, --list",
description: t("create:options.list"),
})
parseList(): boolean {
return true;
}
@Option({
flags: "-f, --from <repo>",
description: t("create:options.from"),
})
parseFrom(val: string): string {
return val;
}
@Option({
flags: "-r, --ref <ref>",
description: t("create:options.ref"),
})
parseRef(val: string): string {
return val;
}
@Option({
flags: "-v, --verbose",
description: t("common.options.verbose"),
})
parseVerbose(_val: string, previous: VerboseLevel = 0): VerboseLevel {
const current = typeof previous === "number" ? previous : previous ? 1 : 0;
return Math.min(current + 1, 2) as VerboseLevel;
}
protected async listTemplates(options: CreateOptions, verbose: VerboseLevel): Promise<void> {
if (options.from) {
await this.createService.ensureRemoteTemplates(options.from, options.ref, verbose);
}
const templates = await this.createService.getAvailableTemplates(options);
console.log(t("create:availableTemplates"));
for (const tpl of templates) {
console.log(` - ${tpl}`);
}
}
protected async showHelp(): Promise<void> {
const templates = await this.createService.getAvailableTemplates();
console.log("Usage: spaceflow create <template> <name> [options]");
console.log("");
console.log(t("create:availableTemplates"));
for (const tpl of templates) {
console.log(` - ${tpl}`);
}
console.log("");
console.log("Options:");
console.log(` -d, --directory <dir> ${t("create:options.directory")}`);
console.log(` -l, --list ${t("create:options.list")}`);
console.log(` -f, --from <repo> ${t("create:options.from")}`);
console.log(` -r, --ref <ref> ${t("create:options.ref")}`);
console.log("");
console.log("Examples:");
console.log(" spaceflow create command my-cmd");
console.log(" spaceflow create skills my-skill");
console.log(" spaceflow create command my-cmd -d ./plugins/my-cmd");
console.log(" spaceflow create -f https://github.com/user/templates command my-cmd");
console.log(" spaceflow create -f git@gitea.example.com:org/tpl.git -r v1.0 api my-api");
}
}

View File

@@ -0,0 +1,8 @@
import { Module } from "@nestjs/common";
import { CreateCommand } from "./create.command";
import { CreateService } from "./create.service";
@Module({
providers: [CreateCommand, CreateService],
})
export class CreateModule {}

View File

@@ -0,0 +1,320 @@
import { Injectable } from "@nestjs/common";
import { mkdir, writeFile, readFile, readdir, stat } from "fs/promises";
import { join, resolve } from "path";
import { existsSync } from "fs";
import { execSync } from "child_process";
import { createHash } from "crypto";
import { homedir } from "os";
import { shouldLog, type VerboseLevel, t } from "@spaceflow/core";
export interface CreateOptions {
directory?: string;
from?: string;
ref?: string;
}
interface TemplateContext {
name: string;
pascalName: string;
camelName: string;
kebabName: string;
}
/**
* 创建插件服务
*/
@Injectable()
export class CreateService {
// 缓存当前使用的远程模板目录
private remoteTemplatesDir: string | null = null;
/**
* 获取缓存目录路径
*/
protected getCacheDir(): string {
return join(homedir(), ".cache", "spaceflow", "templates");
}
/**
* 计算仓库 URL 的哈希值(用于缓存目录名)
*/
protected getRepoHash(repoUrl: string): string {
return createHash("md5").update(repoUrl).digest("hex").slice(0, 12);
}
/**
* 从仓库 URL 提取名称
*/
protected getRepoName(repoUrl: string): string {
// 处理 git@host:org/repo.git 或 https://host/org/repo.git
const match = repoUrl.match(/[/:]([^/]+\/[^/]+?)(?:\.git)?$/);
if (match) {
return match[1].replace("/", "-");
}
return this.getRepoHash(repoUrl);
}
/**
* 确保远程模板仓库已克隆/更新
*/
async ensureRemoteTemplates(
repoUrl: string,
ref?: string,
verbose: VerboseLevel = 1,
): Promise<string> {
const cacheDir = this.getCacheDir();
const repoName = this.getRepoName(repoUrl);
const repoHash = this.getRepoHash(repoUrl);
const targetDir = join(cacheDir, `${repoName}-${repoHash}`);
// 确保缓存目录存在
await mkdir(cacheDir, { recursive: true });
if (existsSync(targetDir)) {
// 已存在,更新仓库
if (shouldLog(verbose, 1)) console.log(t("create:updatingRepo", { url: repoUrl }));
try {
execSync("git fetch --all", { cwd: targetDir, stdio: "pipe" });
if (ref) {
execSync(`git checkout ${ref}`, { cwd: targetDir, stdio: "pipe" });
// 如果是分支,尝试 pull
try {
execSync(`git pull origin ${ref}`, { cwd: targetDir, stdio: "pipe" });
} catch {
// 可能是 tag忽略 pull 错误
}
} else {
execSync("git pull", { cwd: targetDir, stdio: "pipe" });
}
} catch (error) {
if (shouldLog(verbose, 1)) console.warn(t("create:updateFailed"));
}
} else {
// 不存在,克隆仓库
if (shouldLog(verbose, 1)) console.log(t("create:cloningRepo", { url: repoUrl }));
try {
execSync(`git clone ${repoUrl} ${targetDir}`, { stdio: "pipe" });
if (ref) {
execSync(`git checkout ${ref}`, { cwd: targetDir, stdio: "pipe" });
}
} catch (error) {
throw new Error(
t("create:cloneFailed", { error: error instanceof Error ? error.message : error }),
);
}
}
// 设置当前使用的远程模板目录
this.remoteTemplatesDir = targetDir;
if (shouldLog(verbose, 1)) console.log(t("create:repoReady", { dir: targetDir }));
return targetDir;
}
/**
* 获取模板目录路径
*/
protected getTemplatesDir(options?: CreateOptions): string {
// 如果有远程模板目录,优先使用
if (options?.from && this.remoteTemplatesDir) {
return this.remoteTemplatesDir;
}
// 尝试从项目根目录查找 templates
const cwd = process.cwd();
const templatesInCwd = join(cwd, "templates");
if (existsSync(templatesInCwd)) {
return templatesInCwd;
}
// 尝试从父目录查找(在 core 子目录运行时)
const templatesInParent = join(cwd, "..", "templates");
if (existsSync(templatesInParent)) {
return resolve(templatesInParent);
}
// 尝试从 spaceflow 包目录查找
const spaceflowRoot = join(cwd, "node_modules", "spaceflow", "templates");
if (existsSync(spaceflowRoot)) {
return spaceflowRoot;
}
// 回退到相对于当前文件的路径(开发模式)
// 从 core/dist/commands/create 回溯到项目根目录
const devPath = resolve(__dirname, "..", "..", "..", "..", "templates");
if (existsSync(devPath)) {
return devPath;
}
throw new Error(t("create:templatesDirNotFound"));
}
/**
* 获取可用的模板列表
*/
async getAvailableTemplates(options?: CreateOptions): Promise<string[]> {
try {
const templatesDir = this.getTemplatesDir(options);
const entries = await readdir(templatesDir);
const templates: string[] = [];
for (const entry of entries) {
// 跳过隐藏目录
if (entry.startsWith(".")) {
continue;
}
const entryPath = join(templatesDir, entry);
const entryStat = await stat(entryPath);
if (entryStat.isDirectory()) {
templates.push(entry);
}
}
return templates;
} catch {
return [];
}
}
/**
* 基于模板创建插件
*/
async createFromTemplate(
template: string,
name: string,
options: CreateOptions,
verbose: VerboseLevel = 1,
): Promise<void> {
const cwd = process.cwd();
// 默认目录为 <template>/<name>
const targetDir = options.directory || join(template, name);
const fullPath = join(cwd, targetDir);
if (shouldLog(verbose, 1)) {
console.log(t("create:creatingPlugin", { template, name }));
console.log(t("create:targetDir", { dir: targetDir }));
}
// 检查目录是否已存在
if (existsSync(fullPath)) {
throw new Error(t("create:dirExists", { dir: targetDir }));
}
// 从模板生成文件
const templatesDir = this.getTemplatesDir(options);
const templateDir = join(templatesDir, template);
if (!existsSync(templateDir)) {
const available = await this.getAvailableTemplates(options);
throw new Error(
t("create:templateNotFound", {
template,
available: available.join(", ") || t("create:noTemplatesAvailable"),
}),
);
}
const context = this.createContext(name);
await this.copyTemplateDir(templateDir, fullPath, context, verbose);
if (shouldLog(verbose, 1)) {
console.log(t("create:pluginCreated", { template, name }));
console.log("");
console.log(t("create:nextSteps"));
console.log(` cd ${targetDir}`);
}
}
/**
* 创建模板上下文
*/
protected createContext(name: string): TemplateContext {
return {
name,
pascalName: this.toPascalCase(name),
camelName: this.toCamelCase(name),
kebabName: this.toKebabCase(name),
};
}
/**
* 递归复制模板目录
*/
protected async copyTemplateDir(
templateDir: string,
targetDir: string,
context: TemplateContext,
verbose: VerboseLevel = 1,
): Promise<void> {
// 确保目标目录存在
await mkdir(targetDir, { recursive: true });
const entries = await readdir(templateDir);
for (const entry of entries) {
const templatePath = join(templateDir, entry);
const entryStat = await stat(templatePath);
if (entryStat.isDirectory()) {
// 递归处理子目录
const targetSubDir = join(targetDir, entry);
await this.copyTemplateDir(templatePath, targetSubDir, context, verbose);
} else if (entry.endsWith(".hbs")) {
// 处理模板文件
const content = await readFile(templatePath, "utf-8");
const rendered = this.renderTemplate(content, context);
// 计算目标文件名(移除 .hbs 后缀,替换 __name__ 占位符)
let targetFileName = entry.slice(0, -4); // 移除 .hbs
targetFileName = targetFileName.replace(/__name__/g, context.kebabName);
const targetPath = join(targetDir, targetFileName);
await writeFile(targetPath, rendered);
if (shouldLog(verbose, 1)) console.log(t("create:fileCreated", { file: targetFileName }));
} else {
// 直接复制非模板文件
const content = await readFile(templatePath);
const targetPath = join(targetDir, entry);
await writeFile(targetPath, content);
if (shouldLog(verbose, 1)) console.log(t("create:fileCopied", { file: entry }));
}
}
}
/**
* 渲染模板(简单的 Handlebars 风格替换)
*/
protected renderTemplate(template: string, context: TemplateContext): string {
return template
.replace(/\{\{name\}\}/g, context.name)
.replace(/\{\{pascalName\}\}/g, context.pascalName)
.replace(/\{\{camelName\}\}/g, context.camelName)
.replace(/\{\{kebabName\}\}/g, context.kebabName);
}
/**
* 转换为 PascalCase
*/
protected toPascalCase(str: string): string {
return str
.split(/[-_]/)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join("");
}
/**
* 转换为 camelCase
*/
protected toCamelCase(str: string): string {
const pascal = this.toPascalCase(str);
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
}
/**
* 转换为 kebab-case
*/
protected toKebabCase(str: string): string {
return str
.replace(/([a-z])([A-Z])/g, "$1-$2")
.replace(/[_\s]+/g, "-")
.toLowerCase();
}
}

View File

@@ -0,0 +1,28 @@
import type {
SpaceflowExtension,
SpaceflowExtensionMetadata,
ExtensionModuleType,
} from "@spaceflow/core";
import { t } from "@spaceflow/core";
import { CreateModule } from "./create.module";
export const createMetadata: SpaceflowExtensionMetadata = {
name: "create",
commands: ["create"],
version: "1.0.0",
description: t("create:extensionDescription"),
};
export class CreateExtension implements SpaceflowExtension {
getMetadata(): SpaceflowExtensionMetadata {
return createMetadata;
}
getModule(): ExtensionModuleType {
return CreateModule;
}
}
export * from "./create.module";
export { CreateCommand } from "./create.command";
export { CreateService } from "./create.service";

View File

@@ -0,0 +1,55 @@
import { Command, CommandRunner, Option } from "nest-commander";
import { t } from "@spaceflow/core";
import { BuildService } from "../build/build.service";
import type { VerboseLevel } from "@spaceflow/core";
/**
* 开发命令
*
* 用于开发 skills 目录下的插件包(监听模式)
*
* 用法:
* spaceflow dev [skill] # 监听并构建指定或所有插件
*/
export interface DevOptions {
verbose?: VerboseLevel;
}
@Command({
name: "dev",
arguments: "[skill]",
description: t("dev:description"),
})
export class DevCommand extends CommandRunner {
constructor(private readonly buildService: BuildService) {
super();
}
async run(passedParams: string[], options: DevOptions): Promise<void> {
const skill = passedParams[0];
const verbose = options.verbose ?? true;
try {
await this.buildService.watch(skill, verbose);
} catch (error) {
if (error instanceof Error) {
console.error(t("dev:startFailed", { error: error.message }));
if (error.stack) {
console.error(t("common.stackTrace", { stack: error.stack }));
}
} else {
console.error(t("dev:startFailed", { error }));
}
process.exit(1);
}
}
@Option({
flags: "-v, --verbose",
description: t("common.options.verbose"),
})
parseVerbose(_val: string, previous: VerboseLevel = 0): VerboseLevel {
const current = typeof previous === "number" ? previous : previous ? 1 : 0;
return Math.min(current + 1, 2) as VerboseLevel;
}
}

View File

@@ -0,0 +1,8 @@
import { Module } from "@nestjs/common";
import { DevCommand } from "./dev.command";
import { BuildService } from "../build/build.service";
@Module({
providers: [DevCommand, BuildService],
})
export class DevModule {}

View File

@@ -0,0 +1,27 @@
import type {
SpaceflowExtension,
SpaceflowExtensionMetadata,
ExtensionModuleType,
} from "@spaceflow/core";
import { t } from "@spaceflow/core";
import { DevModule } from "./dev.module";
export const devMetadata: SpaceflowExtensionMetadata = {
name: "dev",
commands: ["dev"],
version: "1.0.0",
description: t("dev:extensionDescription"),
};
export class DevExtension implements SpaceflowExtension {
getMetadata(): SpaceflowExtensionMetadata {
return devMetadata;
}
getModule(): ExtensionModuleType {
return DevModule;
}
}
export * from "./dev.module";
export * from "./dev.command";

View File

@@ -0,0 +1,33 @@
import type {
SpaceflowExtension,
SpaceflowExtensionMetadata,
ExtensionModuleType,
} from "@spaceflow/core";
import { t } from "@spaceflow/core";
import { InstallModule } from "./install.module";
export const installMetadata: SpaceflowExtensionMetadata = {
name: "install",
commands: ["install", "i"],
version: "1.0.0",
description: t("install:extensionDescription"),
};
export class InstallExtension implements SpaceflowExtension {
getMetadata(): SpaceflowExtensionMetadata {
return installMetadata;
}
getModule(): ExtensionModuleType {
return InstallModule;
}
}
export * from "./install.module";
export { InstallCommand, type InstallCommandOptions } from "./install.command";
export {
InstallService,
type InstallOptions,
type InstallContext,
type SourceType,
} from "./install.service";

View File

@@ -0,0 +1,119 @@
import { Command, CommandRunner, Option } from "nest-commander";
import { t } from "@spaceflow/core";
import { InstallService } from "./install.service";
import type { VerboseLevel } from "@spaceflow/core";
export interface InstallCommandOptions {
name?: string;
global?: boolean;
verbose?: VerboseLevel;
ignoreErrors?: boolean; // 忽略错误,不退出进程
}
/**
* 安装技能包命令
*
* 用法:
* spaceflow install <source> [--name <名称>]
* spaceflow install # 更新所有已安装的 skills
*
* 支持的 source 类型:
* - 本地路径: ./skills/publish, skills/my-plugin
* - npm 包: @spaceflow/plugin-review, spaceflow-plugin-deploy
* - git 仓库: git@git.example.com:org/plugin.git
*
* 功能:
* 1. 本地路径:注册到 spaceflow.json
* 2. npm 包:执行 pnpm add <package>
* 3. git 仓库:克隆到 .spaceflow/skills/<name> 并关联到支持的编辑器目录
* 4. 更新 spaceflow.json 的 skills 字段
*/
@Command({
name: "install",
arguments: "[source]",
description: t("install:description"),
})
export class InstallCommand extends CommandRunner {
constructor(protected readonly installService: InstallService) {
super();
}
async run(passedParams: string[], options: InstallCommandOptions): Promise<void> {
const source = passedParams[0];
const isGlobal = options.global ?? false;
const verbose = options.verbose ?? true;
try {
if (isGlobal) {
// 全局安装:必须指定 source
if (!source) {
console.error(t("install:globalNoSource"));
console.error(t("install:globalUsage"));
process.exit(1);
}
await this.installService.installGlobal(
{
source,
name: options.name,
},
verbose,
);
} else if (!source) {
// 本地安装无参数:更新配置文件中的所有依赖
await this.installService.updateAllSkills({ verbose });
} else {
// 本地安装有参数:安装指定的依赖
const context = this.installService.getContext({
source,
name: options.name,
});
await this.installService.execute(context, verbose);
}
} catch (error) {
if (error instanceof Error) {
console.error(t("install:installFailed", { error: error.message }));
if (error.stack) {
console.error(t("common.stackTrace", { stack: error.stack }));
}
} else {
console.error(t("install:installFailed", { error }));
}
if (!options.ignoreErrors) {
process.exit(1);
}
}
}
@Option({
flags: "-n, --name <name>",
description: t("install:options.name"),
})
parseName(val: string): string {
return val;
}
@Option({
flags: "-g, --global",
description: t("install:options.global"),
})
parseGlobal(): boolean {
return true;
}
@Option({
flags: "-v, --verbose",
description: t("common.options.verbose"),
})
parseVerbose(_val: string, previous: VerboseLevel = 0): VerboseLevel {
const current = typeof previous === "number" ? previous : previous ? 1 : 0;
return Math.min(current + 1, 2) as VerboseLevel;
}
@Option({
flags: "--ignore-errors",
description: t("install:options.ignoreErrors"),
})
parseIgnoreErrors(): boolean {
return true;
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from "@nestjs/common";
import { InstallCommand } from "./install.command";
import { InstallService } from "./install.service";
@Module({
providers: [InstallCommand, InstallService],
exports: [InstallService],
})
export class InstallModule {}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
import type {
SpaceflowExtension,
SpaceflowExtensionMetadata,
ExtensionModuleType,
} from "@spaceflow/core";
import { t } from "@spaceflow/core";
import { ListModule } from "./list.module";
export const listMetadata: SpaceflowExtensionMetadata = {
name: "list",
commands: ["list", "ls"],
version: "1.0.0",
description: t("list:extensionDescription"),
};
export class ListExtension implements SpaceflowExtension {
getMetadata(): SpaceflowExtensionMetadata {
return listMetadata;
}
getModule(): ExtensionModuleType {
return ListModule;
}
}
export * from "./list.module";
export * from "./list.command";
export * from "./list.service";

View File

@@ -0,0 +1,40 @@
import { Command, CommandRunner, Option } from "nest-commander";
import { t } from "@spaceflow/core";
import { ListService } from "./list.service";
import type { VerboseLevel } from "@spaceflow/core";
/**
* 列出已安装的技能包
*
* 用法:
* spaceflow list
*
* 输出已安装的所有命令包及其信息
*/
export interface ListOptions {
verbose?: VerboseLevel;
}
@Command({
name: "list",
description: t("list:description"),
})
export class ListCommand extends CommandRunner {
constructor(private readonly listService: ListService) {
super();
}
async run(passedParams: string[], options: ListOptions): Promise<void> {
const verbose = options.verbose ?? true;
await this.listService.execute(verbose);
}
@Option({
flags: "-v, --verbose",
description: t("common.options.verbose"),
})
parseVerbose(_val: string, previous: VerboseLevel = 0): VerboseLevel {
const current = typeof previous === "number" ? previous : previous ? 1 : 0;
return Math.min(current + 1, 2) as VerboseLevel;
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from "@nestjs/common";
import { ListCommand } from "./list.command";
import { ListService } from "./list.service";
import { ExtensionLoaderService } from "../../extension-loader";
@Module({
providers: [ListCommand, ListService, ExtensionLoaderService],
})
export class ListModule {}

View File

@@ -0,0 +1,165 @@
import { Injectable } from "@nestjs/common";
import { readFile } from "fs/promises";
import { join } from "path";
import { existsSync } from "fs";
import { ExtensionLoaderService } from "../../extension-loader";
import {
shouldLog,
type VerboseLevel,
getEditorDirName,
DEFAULT_EDITOR,
getSourceType,
normalizeSource,
t,
} from "@spaceflow/core";
interface SkillInfo {
name: string;
source: string;
type: "npm" | "git" | "local";
commands: string[];
installed: boolean;
}
@Injectable()
export class ListService {
constructor(private readonly extensionLoader: ExtensionLoaderService) {}
/**
* 获取支持的编辑器列表
*/
protected async getSupportedEditors(): Promise<string[]> {
const configPath = join(process.cwd(), "spaceflow.json");
try {
if (!existsSync(configPath)) return [DEFAULT_EDITOR];
const content = await readFile(configPath, "utf-8");
const config = JSON.parse(content);
return config.support || [DEFAULT_EDITOR];
} catch {
return [DEFAULT_EDITOR];
}
}
/**
* 执行列表展示
*/
async execute(verbose: VerboseLevel = 1): Promise<void> {
const cwd = process.cwd();
// 优先检查 .spaceflow/spaceflow.json回退到 spaceflow.json
let configPath = join(cwd, ".spaceflow", "spaceflow.json");
if (!existsSync(configPath)) {
configPath = join(cwd, "spaceflow.json");
}
// 读取配置文件中的 skills
const skills = await this.parseSkillsFromConfig(configPath);
if (Object.keys(skills).length === 0) {
if (shouldLog(verbose, 1)) {
console.log(t("list:noSkills"));
console.log("");
console.log(t("list:installHint"));
console.log(" spaceflow install <npm-package>");
console.log(" spaceflow install <git-url> --name <name>");
}
return;
}
// 获取已加载的 Extension 信息
const loadedExtensions = this.extensionLoader.getLoadedExtensions();
const loadedMap = new Map(loadedExtensions.map((e) => [e.name, e]));
const editors = await this.getSupportedEditors();
// 收集所有 skill 信息
const skillInfos: SkillInfo[] = [];
for (const [name, source] of Object.entries(skills)) {
const type = getSourceType(source);
const installed = await this.checkInstalled(name, source, type, editors);
const loadedExt = loadedMap.get(name);
skillInfos.push({ name, source, type, installed, commands: loadedExt?.commands ?? [] });
}
if (!shouldLog(verbose, 1)) return;
// 计算最大名称宽度用于对齐
const maxNameLen = Math.max(...skillInfos.map((s) => s.name.length), 10);
const installedCount = skillInfos.filter((s) => s.installed).length;
console.log(
t("list:installedExtensions", { installed: installedCount, total: skillInfos.length }) + "\n",
);
for (const skill of skillInfos) {
const icon = skill.installed ? "\x1b[32m✔\x1b[0m" : "\x1b[33m○\x1b[0m";
const typeLabel =
skill.type === "local"
? "\x1b[36mlocal\x1b[0m"
: skill.type === "npm"
? "\x1b[35mnpm\x1b[0m"
: "\x1b[33mgit\x1b[0m";
const displaySource = this.getDisplaySource(skill.source, skill.type);
console.log(` ${icon} ${skill.name.padEnd(maxNameLen + 2)} ${typeLabel} ${displaySource}`);
// 显示命令列表
if (skill.commands.length > 0) {
console.log(
` ${"".padEnd(maxNameLen)} ${t("list:commands", { commands: skill.commands.join(", ") })}`,
);
}
}
console.log("");
}
/**
* 获取用于展示的 source 字符串
*/
private getDisplaySource(source: string, type: "npm" | "git" | "local"): string {
if (type === "local") {
return normalizeSource(source);
}
if (type === "git") {
// 简化 git URL 展示
const match = source.match(/[/:](\w+\/[\w.-]+?)(?:\.git)?$/);
return match ? match[1] : source;
}
return source;
}
/**
* 检查是否已安装
*/
private async checkInstalled(
name: string,
source: string,
type: "npm" | "git" | "local",
editors: string[],
): Promise<boolean> {
const cwd = process.cwd();
if (type === "local") {
const localPath = join(cwd, normalizeSource(source));
return existsSync(localPath);
} else if (type === "npm") {
try {
require.resolve(source);
return true;
} catch {
return false;
}
} else {
const possiblePaths = [join(cwd, "skills", name)];
for (const editor of editors) {
const editorDirName = getEditorDirName(editor);
possiblePaths.push(join(cwd, editorDirName, "skills", name));
possiblePaths.push(join(cwd, editorDirName, "commands", name));
}
return possiblePaths.some((p) => existsSync(p));
}
}
/**
* 从配置文件解析 dependencies
*/
private async parseSkillsFromConfig(configPath: string): Promise<Record<string, string>> {
try {
const content = await readFile(configPath, "utf-8");
const config = JSON.parse(content);
return config.dependencies || {};
} catch {
return {};
}
}
}

View File

@@ -0,0 +1,28 @@
import type {
SpaceflowExtension,
SpaceflowExtensionMetadata,
ExtensionModuleType,
} from "@spaceflow/core";
import { t } from "@spaceflow/core";
import { McpModule } from "./mcp.module";
export const mcpMetadata: SpaceflowExtensionMetadata = {
name: "mcp",
commands: ["mcp"],
version: "1.0.0",
description: t("mcp:extensionDescription"),
};
export class McpExtension implements SpaceflowExtension {
getMetadata(): SpaceflowExtensionMetadata {
return mcpMetadata;
}
getModule(): ExtensionModuleType {
return McpModule;
}
}
export * from "./mcp.command";
export * from "./mcp.service";
export * from "./mcp.module";

View File

@@ -0,0 +1,81 @@
import { Command, CommandRunner, Option, getPackageManager, t } from "@spaceflow/core";
import { McpService } from "./mcp.service";
import type { VerboseLevel } from "@spaceflow/core";
export interface McpOptions {
verbose?: VerboseLevel;
inspector?: boolean;
}
/**
* MCP 命令
* 启动 MCP Server聚合所有已安装 flow 包的 MCP 工具
*
* 使用方式: spaceflow mcp
*/
@Command({
name: "mcp",
description: t("mcp:description"),
})
export class McpCommand extends CommandRunner {
constructor(private readonly mcpService: McpService) {
super();
}
async run(_passedParams: string[], options: McpOptions): Promise<void> {
if (options.inspector) {
await this.runInspector();
} else {
await this.mcpService.startServer(options.verbose);
}
}
private async runInspector(): Promise<void> {
const { spawn } = await import("child_process");
console.error(t("mcp:inspectorStarting"));
console.error(t("mcp:inspectorDebugCmd"));
const pm = getPackageManager();
const dlxCmd = pm === "pnpm" ? "pnpm" : "npx";
const dlxArgs = pm === "pnpm" ? ["dlx"] : ["-y"];
const inspector = spawn(
dlxCmd,
[...dlxArgs, "@modelcontextprotocol/inspector", pm, "space", "mcp"],
{
stdio: "inherit",
shell: true,
env: { ...process.env, MODELCONTEXT_PROTOCOL_INSPECTOR: "true" },
cwd: process.cwd(),
},
);
inspector.on("error", (err) => {
console.error(t("mcp:inspectorFailed", { error: err.message }));
process.exit(1);
});
await new Promise<void>((_resolve) => {
inspector.on("close", (code) => {
process.exit(code || 0);
});
});
}
@Option({
flags: "-v, --verbose",
description: t("common.options.verboseDebug"),
})
parseVerbose(_val: string, previous: VerboseLevel = 0): VerboseLevel {
const current = typeof previous === "number" ? previous : previous ? 1 : 0;
return Math.min(current + 1, 3) as VerboseLevel;
}
@Option({
flags: "-i, --inspector",
description: t("mcp:options.inspector"),
})
parseInspector(): boolean {
return true;
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from "@spaceflow/core";
import { McpCommand } from "./mcp.command";
import { McpService } from "./mcp.service";
import { ExtensionLoaderService } from "../../extension-loader";
@Module({
providers: [ExtensionLoaderService, McpService, McpCommand],
})
export class McpModule {}

View File

@@ -0,0 +1,217 @@
import { Injectable, t } from "@spaceflow/core";
import type { VerboseLevel } from "@spaceflow/core";
import { shouldLog, type McpToolMetadata } from "@spaceflow/core";
import { ModuleRef } from "@nestjs/core";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { ExtensionLoaderService } from "../../extension-loader/extension-loader.service";
@Injectable()
export class McpService {
constructor(
private readonly extensionLoader: ExtensionLoaderService,
private readonly moduleRef: ModuleRef,
) {}
/**
* 启动 MCP Server
* 扫描所有已安装的扩展,收集 MCP 工具并启动服务
*/
async startServer(verbose?: VerboseLevel): Promise<void> {
if (shouldLog(verbose, 1)) {
console.error(t("mcp:scanning"));
}
// 加载所有扩展
const extensions = await this.extensionLoader.discoverAndLoad();
const allTools: Array<{ tool: McpToolMetadata; provider: any }> = [];
if (shouldLog(verbose, 2)) {
console.error(t("mcp:foundExtensions", { count: extensions.length }));
for (const ext of extensions) {
const exportKeys = ext.exports ? Object.keys(ext.exports) : [];
console.error(` - ${ext.name}: exports=[${exportKeys.join(", ")}]`);
}
}
// 收集所有扩展的 MCP 工具
for (const ext of extensions) {
try {
// 使用包的完整导出(而不是 NestJS 模块)
const packageExports = ext.exports || {};
// 扫描模块导出,查找带有 @McpServer 装饰器的类
for (const key of Object.keys(packageExports)) {
const exported = packageExports[key];
// 直接检查静态属性(跨模块可访问)
const hasMcpServer = !!(exported as any)?.__mcp_server__;
if (shouldLog(verbose, 2) && typeof exported === "function") {
console.error(t("mcp:checkingExport", { key, hasMcpServer }));
}
// 检查是否是带有 @McpServer 装饰器的类
if (typeof exported === "function" && hasMcpServer) {
try {
// 优先从 NestJS 容器获取实例(支持依赖注入)
let instance: any;
try {
instance = this.moduleRef.get(exported, { strict: false });
if (shouldLog(verbose, 2)) {
console.error(t("mcp:containerSuccess", { key }));
}
} catch (diError) {
// 容器中没有,尝试直接实例化(可能缺少依赖)
if (shouldLog(verbose, 2)) {
console.error(
t("mcp:containerFailed", {
key,
error: diError instanceof Error ? diError.message : diError,
}),
);
}
instance = new (exported as any)();
}
// 直接读取静态属性获取工具和元数据
const tools: McpToolMetadata[] = (exported as any).__mcp_tools__ || [];
const serverMeta = (exported as any).__mcp_server__;
for (const tool of tools) {
allTools.push({ tool, provider: instance });
}
if (shouldLog(verbose, 1) && tools.length > 0) {
const serverName = serverMeta?.name || ext.name;
console.error(` 📦 ${serverName}: ${tools.map((t) => t.name).join(", ")}`);
}
} catch {
// 实例化失败
}
}
}
} catch (error) {
if (shouldLog(verbose, 2)) {
console.error(t("mcp:loadToolsFailed", { name: ext.name }), error);
}
}
}
if (allTools.length === 0) {
console.error(t("mcp:noToolsFound"));
console.error(t("mcp:noToolsHint"));
process.exit(1);
}
if (shouldLog(verbose, 1)) {
console.error(t("mcp:toolsFound", { count: allTools.length }));
}
// 启动 MCP Server
await this.runServer(allTools, verbose);
}
/**
* 运行 MCP Server
*/
private async runServer(
allTools: Array<{ tool: McpToolMetadata; provider: any }>,
verbose?: VerboseLevel,
): Promise<void> {
const server = new McpServer({ name: "spaceflow", version: "1.0.0" });
// 注册所有工具(使用 v2 API: server.registerTool
for (const { tool, provider } of allTools) {
// 将 JSON Schema 转换为 Zod schema
const schema = this.jsonSchemaToZod(tool.inputSchema);
server.registerTool(
tool.name,
{
description: tool.description,
inputSchema: Object.keys(schema).length > 0 ? z.object(schema) : z.object({}),
},
async (args: any) => {
try {
const result = await provider[tool.methodName](args || {});
return {
content: [
{
type: "text" as const,
text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text" as const,
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
},
);
}
// 启动 stdio 传输
const transport = new StdioServerTransport();
await server.connect(transport);
if (shouldLog(verbose, 1)) {
console.error(t("mcp:serverStarted", { count: allTools.length }));
}
if (process.env.MODELCONTEXT_PROTOCOL_INSPECTOR) {
await new Promise<void>((resolve) => {
process.stdin.on("close", resolve);
process.on("SIGINT", resolve);
process.on("SIGTERM", resolve);
});
}
}
/**
* 将 JSON Schema 转换为 Zod schema
*/
private jsonSchemaToZod(jsonSchema?: Record<string, any>): Record<string, any> {
if (!jsonSchema || !jsonSchema.properties) {
return {};
}
const zodShape: Record<string, any> = {};
for (const [key, prop] of Object.entries(jsonSchema.properties as Record<string, any>)) {
const isRequired = jsonSchema.required?.includes(key);
let zodType: any;
switch (prop.type) {
case "string":
zodType = z.string();
break;
case "number":
zodType = z.number();
break;
case "boolean":
zodType = z.boolean();
break;
case "array":
zodType = z.array(z.any());
break;
default:
zodType = z.any();
}
if (prop.description) {
zodType = zodType.describe(prop.description);
}
zodShape[key] = isRequired ? zodType : zodType.optional();
}
return zodShape;
}
}

View File

@@ -0,0 +1,29 @@
import type {
SpaceflowExtension,
SpaceflowExtensionMetadata,
ExtensionModuleType,
} from "@spaceflow/core";
import { t } from "@spaceflow/core";
import { RunxModule } from "./runx.module";
export const runxMetadata: SpaceflowExtensionMetadata = {
name: "runx",
commands: ["runx", "x"],
version: "1.0.0",
description: t("runx:extensionDescription"),
};
export class RunxExtension implements SpaceflowExtension {
getMetadata(): SpaceflowExtensionMetadata {
return runxMetadata;
}
getModule(): ExtensionModuleType {
return RunxModule;
}
}
export { RunxModule } from "./runx.module";
export { RunxCommand } from "./runx.command";
export { RunxService } from "./runx.service";
export * from "./runx.utils";

View File

@@ -0,0 +1,79 @@
import { Command, CommandRunner, Option } from "nest-commander";
import { t } from "@spaceflow/core";
import { RunxService } from "./runx.service";
import { parseRunxArgs } from "./runx.utils";
import type { VerboseLevel } from "@spaceflow/core";
export interface RunxCommandOptions {
name?: string;
verbose?: VerboseLevel;
}
/**
* runx 命令:全局安装 + 运行命令
*
* 用法:
* spaceflow x <source> [args...]
* spaceflow x ./commands/ci-scripts --help
*
* 功能:
* 1. 全局安装指定的依赖(如果未安装)
* 2. 运行该依赖提供的命令
*/
@Command({
name: "runx",
aliases: ["x"],
arguments: "<source>",
description: t("runx:description"),
})
export class RunxCommand extends CommandRunner {
constructor(private readonly runxService: RunxService) {
super();
}
async run(passedParams: string[], options: RunxCommandOptions): Promise<void> {
// 使用工具函数解析参数
const { source, args } = parseRunxArgs(process.argv);
const verbose = options.verbose ?? true;
if (!source) {
console.error(t("runx:noSource"));
console.error(t("runx:usage"));
process.exit(1);
}
try {
await this.runxService.execute({
source,
name: options.name,
args,
verbose,
});
} catch (error) {
if (error instanceof Error) {
console.error(t("runx:runFailed", { error: error.message }));
if (error.stack) {
console.error(t("common.stackTrace", { stack: error.stack }));
}
} else {
console.error(t("runx:runFailed", { error }));
}
process.exit(1);
}
}
@Option({
flags: "-n, --name <name>",
description: t("runx:options.name"),
})
parseName(val: string): string {
return val;
}
@Option({
flags: "-v, --verbose",
description: t("common.options.verbose"),
})
parseVerbose(_val: string, previous: VerboseLevel = 0): VerboseLevel {
const current = typeof previous === "number" ? previous : previous ? 1 : 0;
return Math.min(current + 1, 2) as VerboseLevel;
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from "@nestjs/common";
import { RunxCommand } from "./runx.command";
import { RunxService } from "./runx.service";
import { InstallModule } from "../install/install.module";
@Module({
imports: [InstallModule],
providers: [RunxCommand, RunxService],
})
export class RunxModule {}

View File

@@ -0,0 +1,167 @@
import { Injectable, Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { join } from "path";
import { existsSync, realpathSync } from "fs";
import { spawn } from "child_process";
import { InstallService } from "../install/install.service";
import { CommandFactory } from "nest-commander";
import type { SpaceflowExtension, VerboseLevel } from "@spaceflow/core";
import {
configLoaders,
getEnvFilePaths,
StorageModule,
OutputModule,
extractName,
getSourceType,
t,
} from "@spaceflow/core";
export interface RunxOptions {
source: string;
name?: string;
args: string[];
verbose?: VerboseLevel;
}
/**
* Runx 服务
* 全局安装依赖后运行命令
*/
@Injectable()
export class RunxService {
constructor(private readonly installService: InstallService) {}
/**
* 执行 runx全局安装 + 运行命令
*/
async execute(options: RunxOptions): Promise<void> {
const { source, args } = options;
const verbose = options.verbose ?? true;
const name = options.name || extractName(source);
const sourceType = getSourceType(source);
// npm 包直接使用 npx 执行
if (sourceType === "npm") {
if (verbose)
console.log(t("runx:runningCommand", { command: `npx ${source} ${args.join(" ")}` }));
await this.runWithNpx(source, args);
return;
}
// 第一步:全局安装(静默模式)
await this.installService.installGlobal(
{
source,
name: options.name,
},
false,
);
// 第二步:运行命令
if (verbose) console.log(t("runx:runningCommand", { command: `${name} ${args.join(" ")}` }));
await this.runCommand(name, args);
}
/**
* 使用 npx 运行 npm 包
*/
private runWithNpx(packageName: string, args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn("npx", [packageName, ...args], {
stdio: "inherit",
shell: true,
});
child.on("close", (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(t("runx:npxExitCode", { package: packageName, code })));
}
});
child.on("error", (err) => {
reject(err);
});
});
}
/**
* 运行已安装的命令
* 创建包含共享模块的新实例并运行
*/
protected async runCommand(name: string, args: string[]): Promise<void> {
const home = process.env.HOME || process.env.USERPROFILE || "~";
const depPath = join(home, ".spaceflow", "node_modules", name);
// 检查命令是否存在
if (!existsSync(depPath)) {
throw new Error(t("runx:commandNotInstalled", { name }));
}
// 解析符号链接获取真实路径
const realDepPath = realpathSync(depPath);
// 检查是否有 dist/index.js
const distPath = join(realDepPath, "dist", "index.js");
if (!existsSync(distPath)) {
throw new Error(t("runx:commandNotBuilt", { name }));
}
// 动态加载插件(使用 Function 构造器绕过 rspack 转换)
const importUrl = `file://${distPath}`;
const dynamicImport = new Function("url", "return import(url)");
const pluginModule = await dynamicImport(importUrl);
const PluginClass = pluginModule.default;
if (!PluginClass) {
throw new Error(t("runx:pluginNoExport", { name }));
}
const extension: SpaceflowExtension = new PluginClass();
const metadata = extension.getMetadata();
const commandModule = extension.getModule();
// 如果插件只有一个命令,且用户没有指定子命令,自动补充
const finalArgs = this.autoCompleteCommand(args, metadata.commands);
// 创建动态模块并运行(包含配置和共享模块)
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: configLoaders,
envFilePath: getEnvFilePaths(),
}),
StorageModule.forFeature(),
OutputModule,
commandModule,
],
})
class DynamicRunxModule {}
// 修改 process.argv 以传递参数
const originalArgv = process.argv;
process.argv = ["node", "runx", ...finalArgs];
try {
await CommandFactory.run(DynamicRunxModule, {
logger: false,
});
} finally {
process.argv = originalArgv;
}
}
/**
* 自动补充命令名
* 如果插件只有一个命令,且用户没有指定子命令,自动在参数前补充命令名
*/
private autoCompleteCommand(args: string[], commands?: string[]): string[] {
// 没有命令列表或为空,直接返回
if (!commands || commands.length === 0) {
return args;
}
// 如果只有一个命令
if (commands.length === 1) {
const cmdName = commands[0];
// 检查用户是否已经指定了该命令
if (args.length === 0 || args[0] !== cmdName) {
// 如果第一个参数是选项(以 - 开头),说明用户没有指定子命令
if (args.length === 0 || args[0].startsWith("-")) {
return [cmdName, ...args];
}
}
}
return args;
}
}

View File

@@ -0,0 +1,83 @@
/**
* Runx 命令参数解析工具
*/
export interface RunxParsedArgs {
cmdIndex: number;
sourceIndex: number;
source: string;
args: string[];
}
/**
* 查找 runx/x 命令在 argv 中的位置
*/
export function findRunxCmdIndex(argv: string[]): number {
return argv.findIndex((arg) => arg === "runx" || arg === "x");
}
/**
* 判断参数是否为 -n/--name 选项
*/
export function isNameOption(arg: string): boolean {
return arg === "-n" || arg === "--name" || arg.startsWith("-n=") || arg.startsWith("--name=");
}
/**
* 判断参数是否为 -n/--name 选项(需要跳过下一个参数)
*/
export function isNameOptionWithValue(arg: string): boolean {
return arg === "-n" || arg === "--name";
}
/**
* 从参数列表中找到 source跳过 -n/--name 选项)
*/
export function findSourceInArgs(args: string[]): { index: number; source: string } {
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (isNameOptionWithValue(arg)) {
i++; // 跳过选项值
continue;
}
if (isNameOption(arg)) {
continue;
}
if (!arg.startsWith("-")) {
return { index: i, source: arg };
}
}
return { index: -1, source: "" };
}
/**
* 解析 runx 命令的完整参数
*/
export function parseRunxArgs(argv: string[]): RunxParsedArgs {
const cmdIndex = findRunxCmdIndex(argv);
if (cmdIndex === -1) {
return { cmdIndex: -1, sourceIndex: -1, source: "", args: [] };
}
const separatorIndex = argv.indexOf("--");
if (separatorIndex === -1) {
// 没有分隔符
const remaining = argv.slice(cmdIndex + 1);
const { index, source } = findSourceInArgs(remaining);
return {
cmdIndex,
sourceIndex: index === -1 ? -1 : cmdIndex + 1 + index,
source,
args: [],
};
}
// 有分隔符
const beforeSeparator = argv.slice(cmdIndex + 1, separatorIndex);
const afterSeparator = argv.slice(separatorIndex + 1);
const { index, source } = findSourceInArgs(beforeSeparator);
return {
cmdIndex,
sourceIndex: index === -1 ? -1 : cmdIndex + 1 + index,
source,
args: afterSeparator,
};
}

View File

@@ -0,0 +1,27 @@
import type {
SpaceflowExtension,
SpaceflowExtensionMetadata,
ExtensionModuleType,
} from "@spaceflow/core";
import { t } from "@spaceflow/core";
import { SchemaModule } from "./schema.module";
export const schemaMetadata: SpaceflowExtensionMetadata = {
name: "schema",
commands: ["schema"],
version: "1.0.0",
description: t("schema:extensionDescription"),
};
export class SchemaExtension implements SpaceflowExtension {
getMetadata(): SpaceflowExtensionMetadata {
return schemaMetadata;
}
getModule(): ExtensionModuleType {
return SchemaModule;
}
}
export * from "./schema.command";
export * from "./schema.module";

View File

@@ -0,0 +1,17 @@
import { Command, CommandRunner } from "nest-commander";
import { t } from "@spaceflow/core";
import { SchemaGeneratorService } from "@spaceflow/core";
@Command({
name: "schema",
description: t("schema:description"),
})
export class SchemaCommand extends CommandRunner {
constructor(private readonly schemaGenerator: SchemaGeneratorService) {
super();
}
async run(): Promise<void> {
this.schemaGenerator.generate();
}
}

View File

@@ -0,0 +1,7 @@
import { Module } from "@nestjs/common";
import { SchemaCommand } from "./schema.command";
@Module({
providers: [SchemaCommand],
})
export class SchemaModule {}

View File

@@ -0,0 +1,28 @@
import type {
SpaceflowExtension,
SpaceflowExtensionMetadata,
ExtensionModuleType,
} from "@spaceflow/core";
import { t } from "@spaceflow/core";
import { SetupModule } from "./setup.module";
export const setupMetadata: SpaceflowExtensionMetadata = {
name: "setup",
commands: ["setup"],
version: "1.0.0",
description: t("setup:extensionDescription"),
};
export class SetupExtension implements SpaceflowExtension {
getMetadata(): SpaceflowExtensionMetadata {
return setupMetadata;
}
getModule(): ExtensionModuleType {
return SetupModule;
}
}
export * from "./setup.command";
export * from "./setup.service";
export * from "./setup.module";

View File

@@ -0,0 +1,47 @@
import { Command, CommandRunner, Option } from "nest-commander";
import { t } from "@spaceflow/core";
import { SetupService } from "./setup.service";
export interface SetupCommandOptions {
global?: boolean;
}
@Command({
name: "setup",
description: t("setup:description"),
})
export class SetupCommand extends CommandRunner {
constructor(private readonly setupService: SetupService) {
super();
}
async run(_passedParams: string[], options: SetupCommandOptions): Promise<void> {
const isGlobal = options.global ?? false;
try {
if (isGlobal) {
await this.setupService.setupGlobal();
} else {
await this.setupService.setupLocal();
}
} catch (error) {
if (error instanceof Error) {
console.error(t("setup:setupFailed", { error: error.message }));
if (error.stack) {
console.error(t("common.stackTrace", { stack: error.stack }));
}
} else {
console.error(t("setup:setupFailed", { error }));
}
process.exit(1);
}
}
@Option({
flags: "-g, --global",
description: t("setup:options.global"),
})
parseGlobal(): boolean {
return true;
}
}

View File

@@ -0,0 +1,8 @@
import { Module } from "@nestjs/common";
import { SetupCommand } from "./setup.command";
import { SetupService } from "./setup.service";
@Module({
providers: [SetupCommand, SetupService],
})
export class SetupModule {}

View File

@@ -0,0 +1,240 @@
import { Injectable } from "@nestjs/common";
import { existsSync, readFileSync, writeFileSync } from "fs";
import { t } from "@spaceflow/core";
import { join } from "path";
import { homedir } from "os";
import stringify from "json-stringify-pretty-compact";
import {
CONFIG_FILE_NAME,
RC_FILE_NAME,
getConfigPath,
readConfigSync,
type SpaceflowConfig,
SchemaGeneratorService,
SPACEFLOW_DIR,
ensureSpaceflowPackageJson,
} from "@spaceflow/core";
import { ConfigService } from "@nestjs/config";
@Injectable()
export class SetupService {
constructor(
private readonly configService: ConfigService,
private readonly schemaGenerator: SchemaGeneratorService,
) {}
/**
* 本地初始化:创建 .spaceflow/ 目录和 package.json
*/
async setupLocal(): Promise<void> {
const cwd = process.cwd();
// 1. 创建 .spaceflow/ 目录和 package.json
const spaceflowDir = join(cwd, SPACEFLOW_DIR);
ensureSpaceflowPackageJson(spaceflowDir, false, cwd);
console.log(t("setup:dirCreated", { dir: spaceflowDir }));
// 2. 创建 spaceflow.json 配置文件(运行时配置)
const configPath = getConfigPath(cwd);
const rcPath = join(cwd, RC_FILE_NAME);
if (!existsSync(configPath) && !existsSync(rcPath)) {
this.schemaGenerator.generate();
const defaultConfig: Partial<SpaceflowConfig> = {
$schema: "./config-schema.json",
support: ["claudeCode"],
};
writeFileSync(configPath, stringify(defaultConfig, { indent: 2 }) + "\n");
console.log(t("setup:configGenerated", { path: configPath }));
} else {
const existingPath = existsSync(rcPath) ? rcPath : configPath;
console.log(t("setup:configExists", { path: existingPath }));
}
}
/**
* 全局初始化:创建 ~/.spaceflow/ 目录和 package.json并合并配置
*/
async setupGlobal(): Promise<void> {
const cwd = process.cwd();
const globalDir = join(homedir(), SPACEFLOW_DIR);
const globalConfigPath = join(globalDir, CONFIG_FILE_NAME);
// 1. 创建 ~/.spaceflow/ 目录和 package.json
ensureSpaceflowPackageJson(globalDir, true, cwd);
console.log(t("setup:dirCreated", { dir: globalDir }));
// 读取本地配置(支持 .spaceflow/spaceflow.json 和 .spaceflowrc
const localConfig = readConfigSync(cwd);
if (Object.keys(localConfig).length > 0) {
console.log(t("setup:localConfigRead"));
}
// 读取本地 .env 文件并解析为配置
const envPath = join(cwd, ".env");
const envConfig = this.parseEnvToConfig(envPath);
const instanceConfig = this.configService.get<Partial<SpaceflowConfig>>("spaceflow") ?? {};
// 合并配置:本地配置(已含全局) < 实例配置 < 环境变量配置
const mergedConfig = this.deepMerge(localConfig, instanceConfig, envConfig);
// 写入全局配置
writeFileSync(globalConfigPath, stringify(mergedConfig, { indent: 2 }) + "\n");
console.log(t("setup:globalConfigGenerated", { path: globalConfigPath }));
// 显示合并的环境变量
if (Object.keys(envConfig).length > 0) {
console.log(t("setup:envConfigMerged"));
this.printConfigTree(envConfig, " ");
}
}
/**
* 解析 .env 文件为配置对象
* 支持嵌套格式SPACEFLOW_GIT_PROVIDER_SERVER_URL -> { gitProvider: { serverUrl: "..." } }
*/
private parseEnvToConfig(envPath: string): Record<string, unknown> {
if (!existsSync(envPath)) {
return {};
}
const config: Record<string, unknown> = {};
try {
const content = readFileSync(envPath, "utf-8");
const lines = content.split("\n");
for (const line of lines) {
const trimmed = line.trim();
// 跳过空行和注释
if (!trimmed || trimmed.startsWith("#")) continue;
const eqIndex = trimmed.indexOf("=");
if (eqIndex === -1) continue;
const key = trimmed.slice(0, eqIndex).trim();
let value = trimmed.slice(eqIndex + 1).trim();
// 移除引号
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
// 只处理 SPACEFLOW_ 前缀的环境变量
if (!key.startsWith("SPACEFLOW_")) continue;
// 转换为嵌套配置
// SPACEFLOW_GIT_PROVIDER_SERVER_URL -> gitProvider.serverUrl
const parts = key
.slice("SPACEFLOW_".length)
.toLowerCase()
.split("_")
.map((part, index) => {
// 第一个部分保持小写,后续部分转为 camelCase
if (index === 0) return part;
return part.charAt(0).toUpperCase() + part.slice(1);
});
// 重新组织为嵌套结构
// 例如: ["git", "Provider", "Server", "Url"] -> { gitProviderServerUrl: value }
// 简化处理:按照常见模式分组
this.setNestedValue(config, parts, value);
}
console.log(t("setup:envRead", { path: envPath }));
} catch {
console.warn(t("setup:envReadFailed", { path: envPath }));
}
return config;
}
/**
* 设置嵌套值
* 例如: ["review", "Gitea", "Server", "Url"] 和 value
* 结果: { review: { giteaServerUrl: value } }
*/
private setNestedValue(obj: Record<string, unknown>, parts: string[], value: string): void {
if (parts.length === 0) return;
// 第一个部分作为顶级 key如 review, commit 等)
const topKey = parts[0];
if (parts.length === 1) {
obj[topKey] = value;
return;
}
// 剩余部分合并为 camelCase 作为嵌套 key
// 例如: ["Gitea", "Server", "Url"] -> giteaServerUrl
const restParts = parts.slice(1);
const nestedKey = restParts
.map((part, index) => (index === 0 ? part.toLowerCase() : part))
.join("");
if (!obj[topKey] || typeof obj[topKey] !== "object") {
obj[topKey] = {};
}
(obj[topKey] as Record<string, unknown>)[nestedKey] = value;
}
/**
* 深度合并对象
*/
private deepMerge<T extends Record<string, unknown>>(...objects: Partial<T>[]): Partial<T> {
const result: Record<string, unknown> = {};
for (const obj of objects) {
for (const key in obj) {
const value = obj[key];
const existing = result[key];
if (
value !== null &&
typeof value === "object" &&
!Array.isArray(value) &&
existing !== null &&
typeof existing === "object" &&
!Array.isArray(existing)
) {
result[key] = this.deepMerge(
existing as Record<string, unknown>,
value as Record<string, unknown>,
);
} else if (value !== undefined) {
result[key] = value;
}
}
}
return result as Partial<T>;
}
/**
* 打印配置树
*/
private printConfigTree(config: Record<string, unknown>, prefix: string): void {
for (const [key, value] of Object.entries(config)) {
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
console.log(`${prefix}${key}:`);
this.printConfigTree(value as Record<string, unknown>, prefix + " ");
} else {
// 隐藏敏感值
const displayValue = this.isSensitiveKey(key) ? "***" : String(value);
console.log(`${prefix}${key}: ${displayValue}`);
}
}
}
/**
* 判断是否为敏感 key
*/
private isSensitiveKey(key: string): boolean {
const sensitivePatterns = ["token", "secret", "password", "key", "apikey"];
const lowerKey = key.toLowerCase();
return sensitivePatterns.some((pattern) => lowerKey.includes(pattern));
}
}

View File

@@ -0,0 +1,28 @@
import type {
SpaceflowExtension,
SpaceflowExtensionMetadata,
ExtensionModuleType,
} from "@spaceflow/core";
import { t } from "@spaceflow/core";
import { UninstallModule } from "./uninstall.module";
export const uninstallMetadata: SpaceflowExtensionMetadata = {
name: "uninstall",
commands: ["uninstall", "un"],
version: "1.0.0",
description: t("uninstall:extensionDescription"),
};
export class UninstallExtension implements SpaceflowExtension {
getMetadata(): SpaceflowExtensionMetadata {
return uninstallMetadata;
}
getModule(): ExtensionModuleType {
return UninstallModule;
}
}
export * from "./uninstall.module";
export * from "./uninstall.command";
export * from "./uninstall.service";

View File

@@ -0,0 +1,73 @@
import { Command, CommandRunner, Option } from "nest-commander";
import { t } from "@spaceflow/core";
import { UninstallService } from "./uninstall.service";
import type { VerboseLevel } from "@spaceflow/core";
export interface UninstallCommandOptions {
global?: boolean;
verbose?: VerboseLevel;
}
/**
* 卸载技能包命令
*
* 用法:
* spaceflow uninstall <name>
*
* 功能:
* 1. 从 spaceflow.json 的 skills 中移除
* 2. npm 包:执行 pnpm remove <package>
* 3. git 仓库:执行 git submodule deinit 并删除目录
*/
@Command({
name: "uninstall",
arguments: "<name>",
description: t("uninstall:description"),
})
export class UninstallCommand extends CommandRunner {
constructor(private readonly uninstallService: UninstallService) {
super();
}
async run(passedParams: string[], options: UninstallCommandOptions): Promise<void> {
const name = passedParams[0];
const isGlobal = options.global ?? false;
const verbose = options.verbose ?? true;
if (!name) {
console.error(t("uninstall:noName"));
process.exit(1);
}
try {
await this.uninstallService.execute(name, isGlobal, verbose);
} catch (error) {
if (error instanceof Error) {
console.error(t("uninstall:uninstallFailed", { error: error.message }));
if (error.stack) {
console.error(t("common.stackTrace", { stack: error.stack }));
}
} else {
console.error(t("uninstall:uninstallFailed", { error }));
}
process.exit(1);
}
}
@Option({
flags: "-g, --global",
description: t("uninstall:options.global"),
})
parseGlobal(): boolean {
return true;
}
@Option({
flags: "-v, --verbose",
description: t("common.options.verbose"),
})
parseVerbose(_val: string, previous: VerboseLevel = 0): VerboseLevel {
const current = typeof previous === "number" ? previous : previous ? 1 : 0;
return Math.min(current + 1, 2) as VerboseLevel;
}
}

View File

@@ -0,0 +1,8 @@
import { Module } from "@nestjs/common";
import { UninstallCommand } from "./uninstall.command";
import { UninstallService } from "./uninstall.service";
@Module({
providers: [UninstallCommand, UninstallService],
})
export class UninstallModule {}

View File

@@ -0,0 +1,168 @@
import { Injectable } from "@nestjs/common";
import { execSync } from "child_process";
import { readFile, rm } from "fs/promises";
import { join } from "path";
import { existsSync, readdirSync } from "fs";
import { shouldLog, type VerboseLevel, t } from "@spaceflow/core";
import { getEditorDirName } from "@spaceflow/core";
import { detectPackageManager } from "@spaceflow/core";
import { getSpaceflowDir } from "@spaceflow/core";
import { getConfigPath, getSupportedEditors, removeDependency } from "@spaceflow/core";
@Injectable()
export class UninstallService {
/**
* 从 .spaceflow/node_modules/ 目录卸载 Extension
* 所有类型的 Extension 都通过 pnpm remove 卸载
*/
private async uninstallExtension(
name: string,
isGlobal: boolean = false,
verbose: VerboseLevel = 1,
): Promise<void> {
const spaceflowDir = getSpaceflowDir(isGlobal);
if (shouldLog(verbose, 1)) {
console.log(t("uninstall:uninstallingExtension", { name }));
console.log(t("uninstall:targetDir", { dir: spaceflowDir }));
}
const pm = detectPackageManager(spaceflowDir);
let cmd: string;
if (pm === "pnpm") {
cmd = `pnpm remove --prefix "${spaceflowDir}" ${name}`;
} else {
cmd = `npm uninstall --prefix "${spaceflowDir}" ${name}`;
}
try {
execSync(cmd, {
cwd: process.cwd(),
stdio: verbose ? "inherit" : "pipe",
});
} catch {
if (shouldLog(verbose, 1)) console.warn(t("uninstall:extensionUninstallFailed", { name }));
}
}
/**
* 执行卸载
*/
async execute(name: string, isGlobal = false, verbose: VerboseLevel = 1): Promise<void> {
if (shouldLog(verbose, 1)) {
if (isGlobal) {
console.log(t("uninstall:uninstallingGlobal", { name }));
} else {
console.log(t("uninstall:uninstalling", { name }));
}
}
const cwd = process.cwd();
const configPath = getConfigPath(cwd);
// 1. 读取配置获取 source
const dependencies = await this.parseSkillsFromConfig(configPath);
let actualName = name;
let config = dependencies[name];
// 如果通过 name 找不到,尝试通过 source 值查找(支持 @spaceflow/review 这样的 npm 包名)
if (!config) {
for (const [key, value] of Object.entries(dependencies)) {
if (value === name) {
actualName = key;
config = value;
if (shouldLog(verbose, 1)) console.log(t("uninstall:foundDependency", { key, value }));
break;
}
}
}
if (!config && !isGlobal) {
throw new Error(t("uninstall:notRegistered", { name }));
}
// 使用实际的 name 进行后续操作
name = actualName;
// 2. 从 .spaceflow/node_modules/ 卸载 Extension
await this.uninstallExtension(name, isGlobal, verbose);
// 3. 删除各个编辑器 commands/skills 中的复制文件
const editors = getSupportedEditors(cwd);
const home = process.env.HOME || process.env.USERPROFILE || "~";
for (const editor of editors) {
const editorDirName = getEditorDirName(editor);
const installRoot = isGlobal ? join(home, editorDirName) : join(cwd, editorDirName);
await this.removeEditorFiles(installRoot, name, verbose);
}
// 4. 从配置文件中移除(仅本地安装)
if (!isGlobal && config) {
this.removeFromConfig(name, cwd, verbose);
}
if (shouldLog(verbose, 1)) console.log(t("uninstall:uninstallDone"));
}
/**
* 删除编辑器目录中的 commands/skills 文件
* install 现在是复制文件到编辑器目录,所以卸载时需要删除这些复制的文件/目录
*/
private async removeEditorFiles(
installRoot: string,
name: string,
verbose: VerboseLevel = 1,
): Promise<void> {
// 删除 skills 目录中的文件
const skillsDir = join(installRoot, "skills");
if (existsSync(skillsDir)) {
const entries = readdirSync(skillsDir);
for (const entry of entries) {
// 匹配 name 或 name-xxx 格式
if (entry === name || entry.startsWith(`${name}-`)) {
const targetPath = join(skillsDir, entry);
if (shouldLog(verbose, 1)) console.log(t("uninstall:deletingSkill", { entry }));
await rm(targetPath, { recursive: true, force: true });
}
}
}
// 删除 commands 目录中的 .md 文件
const commandsDir = join(installRoot, "commands");
if (existsSync(commandsDir)) {
const entries = readdirSync(commandsDir);
for (const entry of entries) {
// 匹配 name.md 或 name-xxx.md 格式
if (entry === `${name}.md` || entry.startsWith(`${name}-`)) {
const targetPath = join(commandsDir, entry);
if (shouldLog(verbose, 1)) console.log(t("uninstall:deletingCommand", { entry }));
await rm(targetPath, { force: true });
}
}
}
}
/**
* 从配置文件解析 dependencies
*/
private async parseSkillsFromConfig(configPath: string): Promise<Record<string, unknown>> {
try {
const content = await readFile(configPath, "utf-8");
const config = JSON.parse(content);
return config.dependencies || {};
} catch {
return {};
}
}
/**
* 从配置文件中移除依赖
*/
private removeFromConfig(name: string, cwd: string, verbose: VerboseLevel = 1): void {
const removed = removeDependency(name, cwd);
if (removed && shouldLog(verbose, 1)) {
console.log(t("uninstall:configUpdated", { path: getConfigPath(cwd) }));
}
}
}

View File

@@ -0,0 +1,28 @@
import type {
SpaceflowExtension,
SpaceflowExtensionMetadata,
ExtensionModuleType,
} from "@spaceflow/core";
import { t } from "@spaceflow/core";
import { UpdateModule } from "./update.module";
export const updateMetadata: SpaceflowExtensionMetadata = {
name: "update",
commands: ["update"],
version: "1.0.0",
description: t("update:extensionDescription"),
};
export class UpdateExtension implements SpaceflowExtension {
getMetadata(): SpaceflowExtensionMetadata {
return updateMetadata;
}
getModule(): ExtensionModuleType {
return UpdateModule;
}
}
export * from "./update.module";
export { UpdateCommand, type UpdateCommandOptions } from "./update.command";
export { UpdateService, type UpdateOptions } from "./update.service";

View File

@@ -0,0 +1,81 @@
import { Command, CommandRunner, Option } from "nest-commander";
import { t } from "@spaceflow/core";
import { UpdateService } from "./update.service";
import type { VerboseLevel } from "@spaceflow/core";
export interface UpdateCommandOptions {
self?: boolean;
verbose?: VerboseLevel;
}
/**
* 更新依赖命令
*
* 用法:
* spaceflow update # 更新所有依赖
* spaceflow update <name> # 更新指定依赖
* spaceflow update --self # 更新 CLI 自身
*
* 功能:
* 1. npm 包:获取最新版本并安装
* 2. git 仓库:拉取最新代码
* 3. --self更新 spaceflow CLI 自身
*/
@Command({
name: "update",
arguments: "[name]",
description: t("update:description"),
})
export class UpdateCommand extends CommandRunner {
constructor(private readonly updateService: UpdateService) {
super();
}
async run(passedParams: string[], options: UpdateCommandOptions): Promise<void> {
const name = passedParams[0];
const verbose = options.verbose ?? 1;
try {
if (options.self) {
await this.updateService.updateSelf(verbose);
return;
}
if (name) {
const success = await this.updateService.updateDependency(name, verbose);
if (!success) {
process.exit(1);
}
} else {
await this.updateService.updateAll(verbose);
}
} catch (error) {
if (error instanceof Error) {
console.error(t("update:updateFailed", { error: error.message }));
if (error.stack) {
console.error(t("common.stackTrace", { stack: error.stack }));
}
} else {
console.error(t("update:updateFailed", { error }));
}
process.exit(1);
}
}
@Option({
flags: "--self",
description: t("update:options.self"),
})
parseSelf(): boolean {
return true;
}
@Option({
flags: "-v, --verbose",
description: t("common.options.verbose"),
})
parseVerbose(_val: string, previous: VerboseLevel = 0): VerboseLevel {
const current = typeof previous === "number" ? previous : previous ? 1 : 0;
return Math.min(current + 1, 2) as VerboseLevel;
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from "@nestjs/common";
import { UpdateCommand } from "./update.command";
import { UpdateService } from "./update.service";
@Module({
providers: [UpdateCommand, UpdateService],
exports: [UpdateService],
})
export class UpdateModule {}

View File

@@ -0,0 +1,375 @@
import { Injectable } from "@nestjs/common";
import { execSync } from "child_process";
import { readFile } from "fs/promises";
import { join } from "path";
import { existsSync } from "fs";
import { shouldLog, type VerboseLevel, t } from "@spaceflow/core";
import { getDependencies } from "@spaceflow/core";
import type { SkillConfig } from "../install/install.service";
export interface UpdateOptions {
name?: string;
self?: boolean;
verbose?: VerboseLevel;
}
@Injectable()
export class UpdateService {
protected getPackageManager(): string {
const cwd = process.cwd();
if (existsSync(join(cwd, "pnpm-lock.yaml"))) {
try {
execSync("pnpm --version", { stdio: "ignore" });
return "pnpm";
} catch {
// pnpm 不可用
}
}
if (existsSync(join(cwd, "yarn.lock"))) {
try {
execSync("yarn --version", { stdio: "ignore" });
return "yarn";
} catch {
// yarn 不可用
}
}
return "npm";
}
protected isPnpmWorkspace(): boolean {
return existsSync(join(process.cwd(), "pnpm-workspace.yaml"));
}
isGitUrl(source: string): boolean {
return (
source.startsWith("git@") ||
(source.startsWith("https://") && source.endsWith(".git")) ||
source.endsWith(".git")
);
}
isLocalPath(source: string): boolean {
return (
source.startsWith("./") ||
source.startsWith("../") ||
source.startsWith("/") ||
source.startsWith("skills/")
);
}
getSourceType(source: string): "npm" | "git" | "local" {
if (this.isLocalPath(source)) return "local";
if (this.isGitUrl(source)) return "git";
return "npm";
}
parseSkillConfig(config: SkillConfig): { source: string; ref?: string } {
if (typeof config === "string") {
return { source: config };
}
return { source: config.source, ref: config.ref };
}
async getLatestNpmVersion(packageName: string): Promise<string | null> {
try {
const result = execSync(`npm view ${packageName} version`, {
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
}).trim();
return result;
} catch {
return null;
}
}
async getCurrentNpmVersion(packageName: string): Promise<string | null> {
const packageJsonPath = join(process.cwd(), "package.json");
if (!existsSync(packageJsonPath)) {
return null;
}
try {
const content = await readFile(packageJsonPath, "utf-8");
const pkg = JSON.parse(content);
const version = pkg.dependencies?.[packageName] || pkg.devDependencies?.[packageName];
if (version) {
return version.replace(/^[\^~>=<]+/, "");
}
return null;
} catch {
return null;
}
}
async updateNpmPackage(packageName: string, verbose: VerboseLevel = 1): Promise<boolean> {
const pm = this.getPackageManager();
const latestVersion = await this.getLatestNpmVersion(packageName);
const currentVersion = await this.getCurrentNpmVersion(packageName);
if (!latestVersion) {
if (shouldLog(verbose, 1)) console.log(t("update:cannotGetLatest", { package: packageName }));
return false;
}
if (currentVersion === latestVersion) {
if (shouldLog(verbose, 1))
console.log(t("update:alreadyLatest", { package: packageName, version: currentVersion }));
return true;
}
if (shouldLog(verbose, 1)) {
console.log(
t("update:versionChange", {
package: packageName,
current: currentVersion || t("update:unknown"),
latest: latestVersion,
}),
);
}
let cmd: string;
if (pm === "pnpm") {
cmd = this.isPnpmWorkspace()
? `pnpm add -wD ${packageName}@latest`
: `pnpm add -D ${packageName}@latest`;
} else if (pm === "yarn") {
cmd = `yarn add -D ${packageName}@latest`;
} else {
cmd = `npm install -D ${packageName}@latest`;
}
try {
execSync(cmd, {
cwd: process.cwd(),
stdio: verbose ? "inherit" : "pipe",
});
return true;
} catch (error) {
if (shouldLog(verbose, 1)) {
console.error(
t("update:npmUpdateFailed", { package: packageName }),
error instanceof Error ? error.message : error,
);
}
return false;
}
}
async updateGitRepo(depPath: string, name: string, verbose: VerboseLevel = 1): Promise<boolean> {
const gitDir = join(depPath, ".git");
if (!existsSync(gitDir)) {
if (shouldLog(verbose, 1)) {
console.log(t("update:notGitRepo", { name }));
console.log(t("update:reinstallHint"));
}
return false;
}
try {
if (shouldLog(verbose, 1)) console.log(t("update:pullingLatest"));
execSync("git pull", {
cwd: depPath,
stdio: verbose ? "inherit" : "pipe",
});
return true;
} catch (error) {
if (shouldLog(verbose, 1)) {
console.error(
t("update:gitUpdateFailed", { name }),
error instanceof Error ? error.message : error,
);
}
return false;
}
}
/**
* 检测 CLI 的安装方式
* 返回: { isGlobal: boolean, pm: 'pnpm' | 'npm' | 'yarn', cwd?: string }
*/
protected detectCliInstallation(): { isGlobal: boolean; pm: string; cwd?: string } {
try {
// 获取当前执行的 CLI 路径
const cliPath = process.argv[1];
// 检查是否在 node_modules 中(本地安装)
if (cliPath.includes("node_modules")) {
// 提取项目根目录node_modules 的父目录)
const nodeModulesIndex = cliPath.indexOf("node_modules");
const projectRoot = cliPath.substring(0, nodeModulesIndex - 1);
// 检测项目使用的包管理器
let pm = "npm";
if (existsSync(join(projectRoot, "pnpm-lock.yaml"))) {
pm = "pnpm";
} else if (existsSync(join(projectRoot, "yarn.lock"))) {
pm = "yarn";
}
return { isGlobal: false, pm, cwd: projectRoot };
}
// 检查是否是全局安装
// 尝试检测 pnpm 全局
try {
const pnpmGlobalDir = execSync("pnpm root -g", {
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
}).trim();
if (cliPath.startsWith(pnpmGlobalDir) || cliPath.includes(".pnpm")) {
return { isGlobal: true, pm: "pnpm" };
}
} catch {
// pnpm 不可用
}
// 尝试检测 npm 全局
try {
const npmGlobalDir = execSync("npm root -g", {
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
}).trim();
if (cliPath.startsWith(npmGlobalDir)) {
return { isGlobal: true, pm: "npm" };
}
} catch {
// npm 不可用
}
// 默认认为是全局安装
return { isGlobal: true, pm: "npm" };
} catch {
return { isGlobal: true, pm: "npm" };
}
}
async updateSelf(verbose: VerboseLevel = 1): Promise<boolean> {
if (shouldLog(verbose, 1)) console.log(t("update:updatingCli"));
const cliPackageName = "@spaceflow/cli";
const installation = this.detectCliInstallation();
if (shouldLog(verbose, 1)) {
console.log(
t("update:installMethod", {
method: installation.isGlobal ? t("update:installGlobal") : t("update:installLocal"),
pm: installation.pm,
}),
);
}
try {
if (installation.isGlobal) {
// 全局安装:使用对应包管理器的全局更新命令
const cmd =
installation.pm === "pnpm"
? `pnpm update -g ${cliPackageName}`
: installation.pm === "yarn"
? `yarn global upgrade ${cliPackageName}`
: `npm update -g ${cliPackageName}`;
if (shouldLog(verbose, 1)) console.log(t("update:executing", { cmd }));
execSync(cmd, { stdio: verbose ? "inherit" : "pipe" });
} else {
// 本地安装:在项目目录中更新
const cwd = installation.cwd || process.cwd();
let cmd: string;
if (installation.pm === "pnpm") {
const isPnpmWorkspace = existsSync(join(cwd, "pnpm-workspace.yaml"));
cmd = isPnpmWorkspace
? `pnpm add -wD ${cliPackageName}@latest`
: `pnpm add -D ${cliPackageName}@latest`;
} else if (installation.pm === "yarn") {
cmd = `yarn add -D ${cliPackageName}@latest`;
} else {
cmd = `npm install -D ${cliPackageName}@latest`;
}
if (shouldLog(verbose, 1)) console.log(t("update:executing", { cmd }));
execSync(cmd, { cwd, stdio: verbose ? "inherit" : "pipe" });
}
if (shouldLog(verbose, 1)) console.log(t("update:cliUpdateDone"));
return true;
} catch (error) {
if (shouldLog(verbose, 1)) {
console.error(t("update:cliUpdateFailed"), error instanceof Error ? error.message : error);
}
return false;
}
}
async updateDependency(name: string, verbose: VerboseLevel = 1): Promise<boolean> {
const cwd = process.cwd();
const dependencies = getDependencies(cwd);
if (!dependencies[name]) {
if (shouldLog(verbose, 1)) console.log(t("update:depNotFound", { name }));
return false;
}
const { source } = this.parseSkillConfig(dependencies[name]);
const sourceType = this.getSourceType(source);
if (shouldLog(verbose, 1)) console.log(t("update:updating", { name }));
if (sourceType === "npm") {
return this.updateNpmPackage(source, verbose);
} else if (sourceType === "git") {
const depPath = join(cwd, ".spaceflow", "deps", name);
return this.updateGitRepo(depPath, name, verbose);
} else {
if (shouldLog(verbose, 1)) console.log(t("update:localNoUpdate"));
return true;
}
}
async updateAll(verbose: VerboseLevel = 1): Promise<void> {
const cwd = process.cwd();
const dependencies = getDependencies(cwd);
if (Object.keys(dependencies).length === 0) {
if (shouldLog(verbose, 1)) console.log(t("update:noDeps"));
return;
}
if (shouldLog(verbose, 1)) {
console.log(t("update:updatingAll", { count: Object.keys(dependencies).length }));
}
let successCount = 0;
let failCount = 0;
for (const [name, config] of Object.entries(dependencies)) {
const { source } = this.parseSkillConfig(config);
const sourceType = this.getSourceType(source);
console.log(`\n📦 ${name}`);
let success = false;
if (sourceType === "npm") {
success = await this.updateNpmPackage(source, verbose);
} else if (sourceType === "git") {
const depPath = join(cwd, ".spaceflow", "deps", name);
success = await this.updateGitRepo(depPath, name, verbose);
} else {
if (shouldLog(verbose, 1)) console.log(t("update:localNoUpdate"));
success = true;
}
if (success) {
successCount++;
} else {
failCount++;
}
}
console.log("\n" + t("update:updateComplete", { success: successCount, fail: failCount }));
}
}

View File

@@ -0,0 +1,338 @@
import { Injectable } from "@nestjs/common";
import * as path from "path";
import * as fs from "fs";
import { createRequire } from "module";
import { homedir } from "os";
import type {
LoadedExtension,
ExtensionModuleType,
SpaceflowExtension,
ExtensionDependencies,
} from "@spaceflow/core";
import {
registerPluginConfig,
registerPluginSchema,
t,
SPACEFLOW_DIR,
PACKAGE_JSON,
} from "@spaceflow/core";
@Injectable()
export class ExtensionLoaderService {
private loadedExtensions: Map<string, LoadedExtension> = new Map();
/**
* 注册内部 Extension用于内置命令
* 与外部 Extension 使用相同的接口,但不需要从文件系统加载
*/
registerInternalExtension(extension: SpaceflowExtension): LoadedExtension {
const metadata = extension.getMetadata();
// 注册 Extension 配置到全局注册表
if (metadata.configKey) {
registerPluginConfig({
name: metadata.name,
configKey: metadata.configKey,
configDependencies: metadata.configDependencies,
configSchema: metadata.configSchema,
});
// 注册 schema如果有
if (metadata.configSchema) {
registerPluginSchema({
configKey: metadata.configKey,
schemaFactory: metadata.configSchema as () => any,
description: metadata.description,
});
}
}
const loadedExtension: LoadedExtension = {
name: metadata.name,
source: "internal",
module: extension.getModule(),
commands: metadata.commands,
configKey: metadata.configKey,
configDependencies: metadata.configDependencies,
configSchema: metadata.configSchema,
version: metadata.version,
description: metadata.description,
};
this.loadedExtensions.set(metadata.name, loadedExtension);
return loadedExtension;
}
/**
* 批量注册内部 Extension
*/
registerInternalExtensions(extensions: SpaceflowExtension[]): LoadedExtension[] {
return extensions.map((ext) => this.registerInternalExtension(ext));
}
/**
* 从 .spaceflow/package.json 发现并加载所有 Extension
* 优先级:项目 .spaceflow/ > 全局 ~/.spaceflow/
*/
async discoverAndLoad(): Promise<LoadedExtension[]> {
const extensions: LoadedExtension[] = [];
// 获取所有 .spaceflow 目录(按优先级从低到高)
const spaceflowDirs = this.getSpaceflowDirs();
// 收集所有 dependencies后面的覆盖前面的
const allDependencies: ExtensionDependencies = {};
for (const dir of spaceflowDirs) {
const deps = this.readDependencies(dir);
Object.assign(allDependencies, deps);
}
// 需要跳过的核心依赖(不是 Extension
const corePackages = ["@spaceflow/core", "@spaceflow/cli"];
// 加载所有 Extension
for (const [name, version] of Object.entries(allDependencies)) {
// 跳过核心包
if (corePackages.includes(name)) {
continue;
}
try {
const extension = await this.loadExtension(name, version);
if (extension) {
extensions.push(extension);
this.loadedExtensions.set(name, extension);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.warn(t("extensionLoader.loadFailed", { name, error: message }));
}
}
return extensions;
}
/**
* 获取 .spaceflow 目录列表(按优先级从低到高)
*/
getSpaceflowDirs(): string[] {
const dirs: string[] = [];
// 1. 全局 ~/.spaceflow/
const globalDir = path.join(homedir(), SPACEFLOW_DIR);
if (fs.existsSync(globalDir)) {
dirs.push(globalDir);
}
// 2. 项目 .spaceflow/
const localDir = path.join(process.cwd(), SPACEFLOW_DIR);
if (fs.existsSync(localDir)) {
dirs.push(localDir);
}
return dirs;
}
/**
* 从 .spaceflow/package.json 读取 dependencies
*/
readDependencies(spaceflowDir: string): ExtensionDependencies {
const packageJsonPath = path.join(spaceflowDir, PACKAGE_JSON);
if (!fs.existsSync(packageJsonPath)) {
return {};
}
try {
const content = fs.readFileSync(packageJsonPath, "utf-8");
const pkg = JSON.parse(content);
return pkg.dependencies || {};
} catch {
return {};
}
}
/**
* 检查包是否是一个有效的 flow 类型 Extension
* 格式spaceflow.exports 或 spaceflow.type === "flow"
*/
private isValidExtension(name: string, spaceflowDir: string): boolean {
const nodeModulesPath = path.join(spaceflowDir, "node_modules", name);
const packageJsonPath = path.join(nodeModulesPath, PACKAGE_JSON);
if (!fs.existsSync(packageJsonPath)) {
return false;
}
try {
const content = fs.readFileSync(packageJsonPath, "utf-8");
const pkg = JSON.parse(content);
const spaceflowConfig = pkg.spaceflow;
if (!spaceflowConfig) {
return false;
}
// 完整格式:检查 exports 中是否有 flow 类型
if (spaceflowConfig.exports) {
return Object.values(spaceflowConfig.exports).some(
(exp: any) => !exp.type || exp.type === "flow",
);
}
// 简化格式:检查 type 是否为 flow默认
if (spaceflowConfig.entry) {
return !spaceflowConfig.type || spaceflowConfig.type === "flow";
}
return false;
} catch {
return false;
}
}
/**
* 加载单个 Extension
*/
async loadExtension(name: string, version: string): Promise<LoadedExtension | null> {
// 尝试从项目 .spaceflow/node_modules 加载
const localSpaceflowDir = path.join(process.cwd(), SPACEFLOW_DIR);
// 先检查是否是有效的 Extension
if (!this.isValidExtension(name, localSpaceflowDir)) {
const globalSpaceflowDir = path.join(homedir(), SPACEFLOW_DIR);
if (!this.isValidExtension(name, globalSpaceflowDir)) {
// 不是有效的 Extension静默跳过可能是 skills 包)
return null;
}
}
let extensionModule = await this.tryLoadFromDir(name, localSpaceflowDir);
// 如果本地没有,尝试从全局 ~/.spaceflow/node_modules 加载
if (!extensionModule) {
const globalSpaceflowDir = path.join(homedir(), SPACEFLOW_DIR);
extensionModule = await this.tryLoadFromDir(name, globalSpaceflowDir);
}
if (!extensionModule) {
console.warn(`⚠️ Extension ${name} 未找到`);
return null;
}
try {
const ExtensionClass =
extensionModule.default || extensionModule[Object.keys(extensionModule)[0]];
if (!ExtensionClass) {
console.warn(`⚠️ Extension ${name} 没有导出有效的 Extension 类`);
return null;
}
const extensionInstance: SpaceflowExtension = new ExtensionClass();
const metadata = extensionInstance.getMetadata();
// 注册 Extension 配置到全局注册表
if (metadata.configKey) {
registerPluginConfig({
name: metadata.name,
configKey: metadata.configKey,
configDependencies: metadata.configDependencies,
configSchema: metadata.configSchema,
});
// 注册 schema如果有
if (metadata.configSchema) {
registerPluginSchema({
configKey: metadata.configKey,
schemaFactory: metadata.configSchema as () => any,
description: metadata.description,
});
}
}
return {
name,
source: `${name}@${version}`,
module: extensionInstance.getModule(),
exports: extensionModule,
commands: metadata.commands,
configKey: metadata.configKey,
configDependencies: metadata.configDependencies,
configSchema: metadata.configSchema,
version: metadata.version,
description: metadata.description,
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`加载 Extension 模块失败: ${message}`);
}
}
/**
* 尝试从指定的 .spaceflow 目录加载 Extension
*/
private async tryLoadFromDir(name: string, spaceflowDir: string): Promise<any | null> {
const packageJsonPath = path.join(spaceflowDir, PACKAGE_JSON);
if (!fs.existsSync(packageJsonPath)) {
return null;
}
try {
// 使用 .spaceflow/package.json 作为基础路径创建 require
const localRequire = createRequire(packageJsonPath);
const resolvedPath = localRequire.resolve(name);
// 使用 Function 构造器来避免 rspack 转换这个 import
const dynamicImport = new Function("url", "return import(url)");
return await dynamicImport(`file://${resolvedPath}`);
} catch {
return null;
}
}
/**
* 获取已加载的 Extension
*/
getLoadedExtensions(): LoadedExtension[] {
return Array.from(this.loadedExtensions.values());
}
/**
* 获取所有可用命令
*/
getAvailableCommands(): string[] {
const commands: string[] = [];
for (const extension of this.loadedExtensions.values()) {
commands.push(...extension.commands);
}
return commands;
}
/**
* 根据命令名查找 Extension
*/
findExtensionByCommand(command: string): LoadedExtension | undefined {
for (const extension of this.loadedExtensions.values()) {
if (extension.commands.includes(command)) {
return extension;
}
}
return undefined;
}
/**
* 获取所有 Extension 模块(用于动态注入)
*/
getExtensionModules(): ExtensionModuleType[] {
return this.getLoadedExtensions().map((e) => e.module);
}
/**
* 清除已加载的 Extension
*/
clear(): void {
this.loadedExtensions.clear();
}
}

View File

@@ -0,0 +1 @@
export * from "./extension-loader.service";

View File

@@ -0,0 +1,38 @@
/**
* 内部 Extension 注册
* 使用与外部 Extension 相同的 SpaceflowExtension 接口
*/
import type { SpaceflowExtension } from "@spaceflow/core";
import { InstallExtension } from "./commands/install";
import { UninstallExtension } from "./commands/uninstall";
import { UpdateExtension } from "./commands/update";
import { BuildExtension } from "./commands/build";
import { DevExtension } from "./commands/dev";
import { CreateExtension } from "./commands/create";
import { ListExtension } from "./commands/list";
import { ClearExtension } from "./commands/clear";
import { RunxExtension } from "./commands/runx";
import { SchemaExtension } from "./commands/schema";
import { CommitExtension } from "./commands/commit";
import { SetupExtension } from "./commands/setup";
import { McpExtension } from "./commands/mcp";
/**
* 内部 Extension 列表
* 所有内置命令都在这里统一注册
*/
export const internalExtensions: SpaceflowExtension[] = [
new InstallExtension(),
new UninstallExtension(),
new UpdateExtension(),
new BuildExtension(),
new DevExtension(),
new CreateExtension(),
new ListExtension(),
new ClearExtension(),
new RunxExtension(),
new SchemaExtension(),
new CommitExtension(),
new SetupExtension(),
new McpExtension(),
];

View File

@@ -0,0 +1,22 @@
{
"description": "Build skill packages in the skills directory",
"options.watch": "Watch for file changes and rebuild automatically",
"buildFailed": "❌ Build failed: {{error}}",
"extensionDescription": "Build plugins/skills",
"noPlugins": "📦 No buildable plugins found",
"startBuilding": "📦 Building {{count}} plugins...",
"buildComplete": "✅ Build complete: {{success}} succeeded, {{fail}} failed",
"startWatching": "👀 Watching {{count}} plugins...",
"stopWatching": "🛑 Stop watching: {{name}}",
"building": "🔨 Building: {{name}}",
"buildSuccess": " ✅ Done ({{duration}}ms)",
"buildFailedWithDuration": " ❌ Failed ({{duration}}ms)",
"buildFailedWithMessage": " ❌ Failed ({{duration}}ms): {{message}}",
"buildWarnings": " ⚠️ Done ({{duration}}ms) - {{count}} warnings",
"watching": "👀 Watching: {{name}}",
"watchError": " ❌ [{{name}}] Error: {{message}}",
"watchBuildFailed": " ❌ [{{name}}] Build failed",
"watchBuildWarnings": " ⚠️ [{{name}}] Build complete - {{count}} warnings",
"watchBuildSuccess": " ✅ [{{name}}] Build complete",
"watchInitFailed": " ❌ [{{name}}] Init failed: {{message}}"
}

View File

@@ -0,0 +1,16 @@
{
"description": "Clear all installed skill packages",
"options.global": "Clear installed content in global directory (~/.spaceflow)",
"clearFailed": "❌ Clear failed: {{error}}",
"extensionDescription": "Clear cache and temporary files",
"clearingGlobal": "🧹 Clearing all skill packages (global)",
"clearing": "🧹 Clearing all skill packages",
"clearDone": "✅ Clear complete",
"spaceflowNotExist": " .spaceflow does not exist, skipping",
"spaceflowNoClean": " .spaceflow has nothing to clean",
"clearingSpaceflow": " Clearing .spaceflow ({{count}} items, preserving config files)",
"deleted": " Deleted: {{entry}}",
"deleteFailed": " Warning: Failed to delete {{entry}}:",
"clearingSkills": " Clearing {{editor}}/skills ({{count}} items)",
"clearingCommands": " Clearing {{editor}}/commands ({{count}} .md files)"
}

View File

@@ -0,0 +1,45 @@
{
"description": "Auto-generate conventional commit messages from staged changes",
"options.dryRun": "Only generate commit message without committing",
"options.noVerify": "Skip pre-commit and commit-msg hooks",
"options.split": "Split into multiple commits by package/logic/domain",
"dryRunMode": "\n🔍 Dry Run mode",
"splitSuccess": "\n✅ Successfully split into {{count}} commits",
"commitSuccess": "\n✅ Commit successful",
"commitFailed": "\n❌ Commit failed: {{error}}",
"extensionDescription": "Smart commit command using LLM to generate commit messages",
"getDiffFailed": "Failed to get staged diff, make sure you are in a git repository",
"noFilesToCommit": "No files to commit",
"noChanges": "No changes found",
"generatingMessage": "📝 Generating commit message with AI...",
"strategyRules": "custom rules",
"strategyRulesFirst": "rules-first",
"strategyPackage": "package directory",
"groupingByStrategy": "🔍 Grouping files by {{strategy}} strategy...",
"detectedGroups": "📦 Detected {{count}} groups of changes",
"scopeChanges": "{{scope}} changes",
"rootChanges": "Root directory changes",
"otherChanges": "Other changes",
"allChanges": "All changes",
"analyzingSplit": "🔍 Analyzing how to split commits within package...",
"dryRunMessage": "[Dry Run] Will commit the following message:\n{{message}}",
"commitFail": "Commit failed",
"stageFilesFailed": "Failed to stage files: {{error}}",
"noWorkingChanges": "No working directory changes",
"noStagedFiles": "No staged files",
"singleCommit": "📦 No split needed, committing as a single commit",
"generatedMessage": "\n📋 Generated commit message:",
"splitIntoCommits": "\n📦 Splitting into {{count}} commits",
"groupItem": " {{index}}. {{reason}}{{pkg}} ({{count}} files)",
"parallelGenerating": "\n🚀 Generating {{count}} commit messages in parallel...",
"allMessagesGenerated": "✅ All commit messages generated",
"generateMessageFailed": "Failed to generate commit messages: {{errors}}",
"resetStagingFailed": "Failed to reset staging area",
"committingGroup": "\n📝 Committing group {{current}}/{{total}}: {{reason}}{{pkg}}",
"skippingNoChanges": "⏭️ Skipping: no actual changes",
"commitMessage": "📋 Commit message:",
"commitItemFailed": "❌ Commit {{index}} failed: {{error}}",
"commitItemFailedDetail": "Commit {{index}} failed: {{error}}\n\nCompleted commits:\n{{committed}}",
"noChangesAll": "No changes found, please modify files or use git add first",
"noStagedFilesHint": "No staged files, please use git add first"
}

View File

@@ -0,0 +1,27 @@
{
"description": "Create a new plugin from template",
"options.directory": "Specify target directory",
"options.list": "List available templates",
"options.from": "Use a remote Git repository as template source",
"options.ref": "Specify Git branch or tag (default: main)",
"noName": "❌ Please specify a name",
"usage": "Usage: spaceflow create <template> <name>",
"createFailed": "❌ Creation failed: {{error}}",
"availableTemplates": "Available templates:",
"extensionDescription": "Create new plugin/skill from template",
"updatingRepo": "🔄 Updating template repository: {{url}}",
"updateFailed": "⚠️ Update failed, using cached version",
"cloningRepo": "📥 Cloning template repository: {{url}}",
"cloneFailed": "Failed to clone repository: {{error}}",
"repoReady": "✅ Template repository ready: {{dir}}",
"templatesDirNotFound": "Templates directory not found",
"creatingPlugin": "📦 Creating {{template}} plugin: {{name}}",
"targetDir": " Directory: {{dir}}",
"dirExists": "Directory already exists: {{dir}}",
"templateNotFound": "Template \"{{template}}\" not found. Available templates: {{available}}",
"noTemplatesAvailable": "none",
"pluginCreated": "✅ {{template}} plugin created: {{name}}",
"nextSteps": "Next steps:",
"fileCreated": " ✓ Created {{file}}",
"fileCopied": " ✓ Copied {{file}}"
}

View File

@@ -0,0 +1,5 @@
{
"description": "Dev mode - watch and auto-rebuild skill packages",
"startFailed": "❌ Dev mode failed to start: {{error}}",
"extensionDescription": "Start plugin dev mode (watch and auto-rebuild)"
}

View File

@@ -0,0 +1,71 @@
{
"description": "Install skill packages (npm or git), update all skills when no arguments",
"options.name": "Specify skill package name (auto-detected from source by default)",
"options.global": "Install to global directory (~/.spaceflow) and link to global editor directories",
"options.ignoreErrors": "Ignore installation errors without exiting",
"globalNoSource": "❌ Global install requires a source argument",
"globalUsage": " Usage: spaceflow install <source> -g [--name <name>]",
"installFailed": "❌ Installation failed: {{error}}",
"extensionDescription": "Install plugins/skills to project",
"envPlaceholder": "<Please fill in {{key}}>",
"registerMcp": " Register MCP Server: {{name}}",
"mcpEnvHint": " ⚠️ Please configure environment variables in {{path}}: {{vars}}",
"installingExtension": "📦 Installing Extension: {{source}}",
"installDone": "✅ Installation complete: {{name}}",
"typeLocal": " Type: local path",
"sourcePath": " Source path: {{path}}",
"typeGit": " Type: git repository",
"sourceUrl": " Source: {{url}}",
"typeNpm": " Type: npm package",
"targetDir": " Target directory: {{dir}}",
"extensionInstallFailed": "Extension installation failed: {{source}}",
"creatingDir": " Creating directory: {{dir}}",
"dirExistsSkip": " Directory exists, skipping clone",
"cloningRepo": " Cloning repository...",
"cloneFailed": "Failed to clone repository: {{error}}",
"removingGit": " Removing .git directory...",
"depsLinkExists": " deps link exists, skipping",
"depsExists": " deps/{{name}} exists, skipping",
"createDepsLink": " Creating deps link: {{dep}} -> {{source}}",
"skillLinkExists": " skills/{{name}} link exists, skipping",
"createSkillLink": " Creating skills link: skills/{{name}} -> {{target}}",
"copySkill": " Copying skills/{{name}}",
"globalInstalling": "🔄 Global install: {{name}} ({{dir}})",
"pluginTypes": " Plugin types: {{types}}",
"globalInstallDone": "\n✅ Global installation complete",
"updatingAll": "🔄 Updating all dependencies...",
"noDeps": " No dependencies in config file",
"foundDeps": " Found {{count}} dependencies",
"installingDeps": "\n📦 Installing dependencies...",
"pmInstallFailed": " Warning: {{pm}} install failed",
"depNotInstalled": " ⚠️ {{name}} not installed successfully, skipping",
"allExtensionsDone": "\n✅ All extensions updated",
"updatedPackageJson": " ✓ Updated .spaceflow/package.json",
"symlinkExists": " ✓ Symlink already exists",
"createSymlink": " Creating symlink: {{target}} -> {{source}}",
"createSymlinkFailed": "Failed to create symlink: {{error}}",
"pullFailed": " Warning: Failed to pull latest code",
"checkoutVersion": " Checking out version: {{ref}}",
"checkoutFailed": " Warning: Failed to checkout version",
"installingDepsEllipsis": " Installing dependencies...",
"depsInstallFailed": " Warning: Dependency installation failed",
"depsInstalled": " ✓ Dependencies installed",
"buildingPlugin": " Building plugin...",
"buildFailed": " Warning: Build failed",
"buildExists": " ✓ Build exists",
"repoExists": " Repository exists, updating...",
"repoUpdateFailed": " Warning: Repository update failed",
"cloneRepoFailed": " Warning: Clone repository failed",
"skillMdExists": " ✓ SKILL.md already exists",
"skillMdGenerated": " ✓ Generated SKILL.md",
"skillMdFailed": " Warning: Failed to generate SKILL.md",
"commandMdGenerated": " ✓ Generated commands/{{name}}.md",
"commandMdFailed": " Warning: Failed to generate commands/{{name}}.md",
"configUpdated": " Config updated: {{path}}",
"configAlreadyExists": " Config already contains this skill package",
"depsUpToDate": " ✓ Dependencies are up to date",
"buildUpToDate": " ✓ Build is up to date",
"detailSection": "Details",
"usageSection": "Usage",
"commandDefault": "{{name}} command"
}

View File

@@ -0,0 +1,8 @@
{
"description": "List installed skill packages",
"extensionDescription": "List installed plugins/skills",
"noSkills": "📦 No skill packages installed",
"installHint": "Install skill packages with:",
"installedExtensions": "📦 Installed extensions ({{installed}}/{{total}}):",
"commands": "Commands: {{commands}}"
}

View File

@@ -0,0 +1,18 @@
{
"description": "Start MCP Server with all installed extension tools",
"options.inspector": "Start MCP Inspector for interactive debugging",
"inspectorStarting": "🔍 Starting MCP Inspector...",
"inspectorDebugCmd": " Debug command: pnpm space mcp",
"inspectorFailed": "❌ Failed to start MCP Inspector: {{error}}",
"extensionDescription": "Start MCP Server with all installed extension tools",
"scanning": "🔍 Scanning installed extensions...",
"foundExtensions": " Found {{count}} extensions",
"checkingExport": " Checking {{key}}: __mcp_server__={{hasMcpServer}}",
"containerSuccess": " ✅ Got {{key}} instance from container",
"containerFailed": " ⚠️ Failed to get {{key}} from container: {{error}}",
"loadToolsFailed": " ⚠️ {{name}}: Failed to load MCP tools",
"noToolsFound": "❌ No MCP tools found",
"noToolsHint": " Hint: Make sure you have installed MCP-enabled extensions that export mcpService or getMcpTools",
"toolsFound": "✅ Found {{count}} MCP tools",
"serverStarted": "🚀 MCP Server started with {{count}} tools"
}

View File

@@ -0,0 +1,13 @@
{
"description": "Install dependency globally and run command",
"options.name": "Specify command name (auto-detected from source by default)",
"noSource": "❌ Please specify the dependency source to run",
"usage": " Usage: spaceflow x <source> [args...]",
"runFailed": "❌ Run failed: {{error}}",
"extensionDescription": "Execute scripts provided by plugins",
"runningCommand": "▶️ Running command: {{command}}",
"npxExitCode": "npx {{package}} exit code: {{code}}",
"commandNotInstalled": "Command {{name}} is not installed",
"commandNotBuilt": "Command {{name}} is not built, missing dist/index.js",
"pluginNoExport": "Plugin {{name}} has no default export"
}

View File

@@ -0,0 +1,4 @@
{
"description": "Generate JSON Schema for configuration files",
"extensionDescription": "Generate JSON Schema for configuration files"
}

View File

@@ -0,0 +1,14 @@
{
"description": "Initialize spaceflow configuration files",
"options.global": "Generate global config to ~/spaceflow/spaceflow.json (merge local .env and spaceflow.json)",
"setupFailed": "❌ Initialization failed: {{error}}",
"extensionDescription": "Initialize spaceflow configuration files",
"dirCreated": "✅ Directory created: {{dir}}",
"configGenerated": "✅ Config file generated: {{path}}",
"configExists": " Config file already exists: {{path}}",
"localConfigRead": "📖 Local config loaded",
"globalConfigGenerated": "✅ Global config generated: {{path}}",
"envConfigMerged": "📝 Merged environment variable config:",
"envRead": "📖 Environment variables loaded: {{path}}",
"envReadFailed": "⚠️ Failed to read .env file: {{path}}"
}

View File

@@ -0,0 +1,18 @@
{
"description": "Uninstall a skill package",
"options.global": "Uninstall from global directory (~/.spaceflow) and clean up editor links",
"noName": "❌ Please specify the skill package name to uninstall",
"uninstallFailed": "❌ Uninstall failed: {{error}}",
"extensionDescription": "Uninstall installed plugins/skills",
"uninstallingExtension": " Uninstalling extension: {{name}}",
"targetDir": " Target directory: {{dir}}",
"extensionUninstallFailed": " Warning: Extension uninstall failed: {{name}}",
"uninstallingGlobal": "🗑️ Uninstalling skill package: {{name}} (global)",
"uninstalling": "🗑️ Uninstalling skill package: {{name}}",
"foundDependency": " Found dependency: {{key}} -> {{value}}",
"notRegistered": "Skill package \"{{name}}\" is not registered in config",
"uninstallDone": "✅ Uninstall complete",
"deletingSkill": " Deleting: skills/{{entry}}",
"deletingCommand": " Deleting: commands/{{entry}}",
"configUpdated": " Config updated: {{path}}"
}

View File

@@ -0,0 +1,28 @@
{
"description": "Update dependencies (latest npm version or git pull)",
"options.self": "Update spaceflow CLI itself",
"updateFailed": "❌ Update failed: {{error}}",
"extensionDescription": "Update dependencies (latest npm version or git pull)",
"cannotGetLatest": " ⚠️ Cannot get latest version of {{package}}",
"alreadyLatest": " ✓ {{package}} is already up to date ({{version}})",
"versionChange": " {{package}}: {{current}} -> {{latest}}",
"unknown": "unknown",
"npmUpdateFailed": " ❌ Failed to update {{package}}:",
"notGitRepo": " ⚠️ {{name}} is not a git repository (.git may have been removed)",
"reinstallHint": " Hint: Use spaceflow install <source> to reinstall",
"pullingLatest": " Pulling latest code...",
"gitUpdateFailed": " ❌ Failed to update {{name}}:",
"updatingCli": "🔄 Updating spaceflow CLI...",
"installMethod": " Install method: {{method}} ({{pm}})",
"installGlobal": "global",
"installLocal": "local",
"executing": " Executing: {{cmd}}",
"cliUpdateDone": "✅ CLI update complete",
"cliUpdateFailed": "❌ CLI update failed:",
"depNotFound": "❌ Dependency not found: {{name}}",
"updating": "📦 Updating: {{name}}",
"localNoUpdate": " ✓ Local path does not need updating",
"noDeps": "📦 No dependencies found",
"updatingAll": "🔄 Updating all dependencies ({{count}})...",
"updateComplete": "✅ Update complete: {{success}} succeeded, {{fail}} failed"
}

132
cli/src/locales/index.ts Normal file
View File

@@ -0,0 +1,132 @@
import { addLocaleResources } from "@spaceflow/core";
import buildZhCN from "./zh-cn/build.json";
import buildEn from "./en/build.json";
import clearZhCN from "./zh-cn/clear.json";
import clearEn from "./en/clear.json";
import commitZhCN from "./zh-cn/commit.json";
import commitEn from "./en/commit.json";
import createZhCN from "./zh-cn/create.json";
import createEn from "./en/create.json";
import devZhCN from "./zh-cn/dev.json";
import devEn from "./en/dev.json";
import installZhCN from "./zh-cn/install.json";
import installEn from "./en/install.json";
import listZhCN from "./zh-cn/list.json";
import listEn from "./en/list.json";
import mcpZhCN from "./zh-cn/mcp.json";
import mcpEn from "./en/mcp.json";
import runxZhCN from "./zh-cn/runx.json";
import runxEn from "./en/runx.json";
import schemaZhCN from "./zh-cn/schema.json";
import schemaEn from "./en/schema.json";
import setupZhCN from "./zh-cn/setup.json";
import setupEn from "./en/setup.json";
import uninstallZhCN from "./zh-cn/uninstall.json";
import uninstallEn from "./en/uninstall.json";
import updateZhCN from "./zh-cn/update.json";
import updateEn from "./en/update.json";
type LocaleResource = Record<string, Record<string, string>>;
/** build 命令 i18n 资源 */
export const buildLocales: LocaleResource = {
"zh-CN": buildZhCN,
en: buildEn,
};
/** clear 命令 i18n 资源 */
export const clearLocales: LocaleResource = {
"zh-CN": clearZhCN,
en: clearEn,
};
/** commit 命令 i18n 资源 */
export const commitLocales: LocaleResource = {
"zh-CN": commitZhCN,
en: commitEn,
};
/** create 命令 i18n 资源 */
export const createLocales: LocaleResource = {
"zh-CN": createZhCN,
en: createEn,
};
/** dev 命令 i18n 资源 */
export const devLocales: LocaleResource = {
"zh-CN": devZhCN,
en: devEn,
};
/** install 命令 i18n 资源 */
export const installLocales: LocaleResource = {
"zh-CN": installZhCN,
en: installEn,
};
/** list 命令 i18n 资源 */
export const listLocales: LocaleResource = {
"zh-CN": listZhCN,
en: listEn,
};
/** mcp 命令 i18n 资源 */
export const mcpLocales: LocaleResource = {
"zh-CN": mcpZhCN,
en: mcpEn,
};
/** runx 命令 i18n 资源 */
export const runxLocales: LocaleResource = {
"zh-CN": runxZhCN,
en: runxEn,
};
/** schema 命令 i18n 资源 */
export const schemaLocales: LocaleResource = {
"zh-CN": schemaZhCN,
en: schemaEn,
};
/** setup 命令 i18n 资源 */
export const setupLocales: LocaleResource = {
"zh-CN": setupZhCN,
en: setupEn,
};
/** uninstall 命令 i18n 资源 */
export const uninstallLocales: LocaleResource = {
"zh-CN": uninstallZhCN,
en: uninstallEn,
};
/** update 命令 i18n 资源 */
export const updateLocales: LocaleResource = {
"zh-CN": updateZhCN,
en: updateEn,
};
/** 所有内部命令 i18n 资源映射 */
const allLocales: Record<string, LocaleResource> = {
build: buildLocales,
clear: clearLocales,
commit: commitLocales,
create: createLocales,
dev: devLocales,
install: installLocales,
list: listLocales,
mcp: mcpLocales,
runx: runxLocales,
schema: schemaLocales,
setup: setupLocales,
uninstall: uninstallLocales,
update: updateLocales,
};
/**
* 立即注册所有内部命令的 i18n 资源
* 确保 @Command 装饰器中的 t() 在模块 import 时即可获取翻译
*/
for (const [ns, resources] of Object.entries(allLocales)) {
addLocaleResources(ns, resources);
}

View File

@@ -0,0 +1,22 @@
{
"description": "构建 skills 目录下的插件包",
"options.watch": "监听文件变化并自动重新构建",
"buildFailed": "❌ 构建失败: {{error}}",
"extensionDescription": "构建插件/技能",
"noPlugins": "📦 没有找到可构建的插件",
"startBuilding": "📦 开始构建 {{count}} 个插件...",
"buildComplete": "✅ 构建完成: {{success}} 成功, {{fail}} 失败",
"startWatching": "👀 开始监听 {{count}} 个插件...",
"stopWatching": "🛑 停止监听: {{name}}",
"building": "🔨 构建: {{name}}",
"buildSuccess": " ✅ 完成 ({{duration}}ms)",
"buildFailedWithDuration": " ❌ 失败 ({{duration}}ms)",
"buildFailedWithMessage": " ❌ 失败 ({{duration}}ms): {{message}}",
"buildWarnings": " ⚠️ 完成 ({{duration}}ms) - {{count}} 警告",
"watching": "👀 监听: {{name}}",
"watchError": " ❌ [{{name}}] 错误: {{message}}",
"watchBuildFailed": " ❌ [{{name}}] 构建失败",
"watchBuildWarnings": " ⚠️ [{{name}}] 构建完成 - {{count}} 警告",
"watchBuildSuccess": " ✅ [{{name}}] 构建完成",
"watchInitFailed": " ❌ [{{name}}] 初始化失败: {{message}}"
}

View File

@@ -0,0 +1,16 @@
{
"description": "清理所有安装的技能包",
"options.global": "清理全局目录 (~/.spaceflow) 的安装内容",
"clearFailed": "❌ 清理失败: {{error}}",
"extensionDescription": "清理缓存和临时文件",
"clearingGlobal": "🧹 清理所有技能包 (全局)",
"clearing": "🧹 清理所有技能包",
"clearDone": "✅ 清理完成",
"spaceflowNotExist": " .spaceflow 不存在,跳过",
"spaceflowNoClean": " .spaceflow 无需清理",
"clearingSpaceflow": " 清理 .spaceflow ({{count}} 项,保留配置文件)",
"deleted": " 删除: {{entry}}",
"deleteFailed": " 警告: 删除 {{entry}} 失败:",
"clearingSkills": " 清理 {{editor}}/skills ({{count}} 项)",
"clearingCommands": " 清理 {{editor}}/commands ({{count}} 个 .md 文件)"
}

View File

@@ -0,0 +1,45 @@
{
"description": "基于暂存区变更自动生成规范的 commit message 并提交",
"options.dryRun": "仅生成 commit message不执行提交",
"options.noVerify": "跳过 pre-commit 和 commit-msg hooks",
"options.split": "按包/逻辑/业务拆分为多个 commit",
"dryRunMode": "\n🔍 Dry Run 模式",
"splitSuccess": "\n✅ 成功拆分为 {{count}} 个 commit",
"commitSuccess": "\n✅ 提交成功",
"commitFailed": "\n❌ 提交失败: {{error}}",
"extensionDescription": "智能 commit 命令,使用 LLM 生成 commit message",
"getDiffFailed": "获取暂存区 diff 失败,请确保在 git 仓库中运行",
"noFilesToCommit": "没有文件需要提交",
"noChanges": "没有变更内容",
"generatingMessage": "📝 正在使用 AI 生成 commit message...",
"strategyRules": "自定义规则",
"strategyRulesFirst": "规则优先",
"strategyPackage": "包目录",
"groupingByStrategy": "🔍 正在按 {{strategy}} 策略分组文件...",
"detectedGroups": "📦 检测到 {{count}} 个分组的变更",
"scopeChanges": "{{scope}} 的变更",
"rootChanges": "根目录变更",
"otherChanges": "其他变更",
"allChanges": "所有变更",
"analyzingSplit": "🔍 正在分析包内如何拆分 commit...",
"dryRunMessage": "[Dry Run] 将提交以下 message:\n{{message}}",
"commitFail": "commit 失败",
"stageFilesFailed": "暂存文件失败: {{error}}",
"noWorkingChanges": "没有工作区改动",
"noStagedFiles": "没有暂存的文件",
"singleCommit": "📦 变更无需拆分,将作为单个 commit 提交",
"generatedMessage": "\n📋 生成的 commit message:",
"splitIntoCommits": "\n📦 将拆分为 {{count}} 个 commit",
"groupItem": " {{index}}. {{reason}}{{pkg}} ({{count}} 个文件)",
"parallelGenerating": "\n🚀 并行生成 {{count}} 个 commit message...",
"allMessagesGenerated": "✅ 所有 commit message 生成完成",
"generateMessageFailed": "生成 commit message 失败: {{errors}}",
"resetStagingFailed": "重置暂存区失败",
"committingGroup": "\n📝 正在提交第 {{current}}/{{total}} 组: {{reason}}{{pkg}}",
"skippingNoChanges": "⏭️ 跳过: 没有实际变更",
"commitMessage": "📋 Commit message:",
"commitItemFailed": "❌ Commit {{index}} 失败: {{error}}",
"commitItemFailedDetail": "第 {{index}} 个 commit 失败: {{error}}\n\n已完成的提交:\n{{committed}}",
"noChangesAll": "没有任何改动,请先修改文件或使用 git add 添加文件",
"noStagedFilesHint": "没有暂存的文件,请先使用 git add 添加文件"
}

View File

@@ -0,0 +1,27 @@
{
"description": "基于模板创建新的插件",
"options.directory": "指定创建目录",
"options.list": "列出可用的模板",
"options.from": "使用远程 Git 仓库作为模板源",
"options.ref": "指定 Git 分支或标签(默认: main",
"noName": "❌ 请指定名称",
"usage": "用法: spaceflow create <template> <name>",
"createFailed": "❌ 创建失败: {{error}}",
"availableTemplates": "可用模板:",
"extensionDescription": "创建新的插件/技能模板",
"updatingRepo": "🔄 更新模板仓库: {{url}}",
"updateFailed": "⚠️ 更新失败,使用缓存版本",
"cloningRepo": "📥 克隆模板仓库: {{url}}",
"cloneFailed": "克隆仓库失败: {{error}}",
"repoReady": "✅ 模板仓库就绪: {{dir}}",
"templatesDirNotFound": "找不到模板目录",
"creatingPlugin": "📦 创建 {{template}} 插件: {{name}}",
"targetDir": " 目录: {{dir}}",
"dirExists": "目录已存在: {{dir}}",
"templateNotFound": "模板 \"{{template}}\" 不存在。可用模板: {{available}}",
"noTemplatesAvailable": "无",
"pluginCreated": "✅ {{template}} 插件创建完成: {{name}}",
"nextSteps": "下一步:",
"fileCreated": " ✓ 创建 {{file}}",
"fileCopied": " ✓ 复制 {{file}}"
}

View File

@@ -0,0 +1,5 @@
{
"description": "开发模式 - 监听 skills 目录下的插件包并自动重新构建",
"startFailed": "❌ 开发模式启动失败: {{error}}",
"extensionDescription": "启动插件开发模式(监听文件变化并自动重新构建)"
}

View File

@@ -0,0 +1,71 @@
{
"description": "安装技能包(支持 npm 包和 git 仓库),不带参数时更新配置文件中的所有 skills",
"options.name": "指定技能包名称(默认从 source 自动提取)",
"options.global": "安装到全局目录 (~/.spaceflow) 并关联到全局编辑器目录",
"options.ignoreErrors": "忽略安装错误,不退出进程",
"globalNoSource": "❌ 全局安装必须指定 source 参数",
"globalUsage": " 用法: spaceflow install <source> -g [--name <名称>]",
"installFailed": "❌ 安装失败: {{error}}",
"extensionDescription": "安装插件/技能到项目中",
"envPlaceholder": "<请填写 {{key}}>",
"registerMcp": " 注册 MCP Server: {{name}}",
"mcpEnvHint": " ⚠️ 请在 {{path}} 中配置环境变量: {{vars}}",
"installingExtension": "📦 安装 Extension: {{source}}",
"installDone": "✅ 安装完成: {{name}}",
"typeLocal": " 类型: 本地路径",
"sourcePath": " 源路径: {{path}}",
"typeGit": " 类型: git 仓库",
"sourceUrl": " 源: {{url}}",
"typeNpm": " 类型: npm 包",
"targetDir": " 目标目录: {{dir}}",
"extensionInstallFailed": "Extension 安装失败: {{source}}",
"creatingDir": " 创建目录: {{dir}}",
"dirExistsSkip": " 目录已存在,跳过克隆",
"cloningRepo": " 克隆仓库...",
"cloneFailed": "克隆仓库失败: {{error}}",
"removingGit": " 移除 .git 目录...",
"depsLinkExists": " deps 链接已存在,跳过",
"depsExists": " deps/{{name}} 已存在,跳过",
"createDepsLink": " 创建 deps 链接: {{dep}} -> {{source}}",
"skillLinkExists": " skills/{{name}} 链接已存在,跳过",
"createSkillLink": " 创建 skills 链接: skills/{{name}} -> {{target}}",
"copySkill": " 复制 skills/{{name}}",
"globalInstalling": "🔄 全局安装: {{name}} ({{dir}})",
"pluginTypes": " 插件类型: {{types}}",
"globalInstallDone": "\n✅ 全局安装完成",
"updatingAll": "🔄 更新所有依赖...",
"noDeps": " 配置文件中没有 dependencies",
"foundDeps": " 找到 {{count}} 个依赖",
"installingDeps": "\n📦 安装依赖...",
"pmInstallFailed": " 警告: {{pm}} install 失败",
"depNotInstalled": " ⚠️ {{name}} 未安装成功,跳过",
"allExtensionsDone": "\n✅ 所有 Extension 更新完成",
"updatedPackageJson": " ✓ 更新 .spaceflow/package.json",
"symlinkExists": " ✓ 符号链接已存在",
"createSymlink": " 创建符号链接: {{target}} -> {{source}}",
"createSymlinkFailed": "创建符号链接失败: {{error}}",
"pullFailed": " 警告: 拉取最新代码失败",
"checkoutVersion": " 切换到版本: {{ref}}",
"checkoutFailed": " 警告: 切换版本失败",
"installingDepsEllipsis": " 安装依赖...",
"depsInstallFailed": " 警告: 依赖安装失败",
"depsInstalled": " ✓ 依赖已安装",
"buildingPlugin": " 构建插件...",
"buildFailed": " 警告: 构建失败",
"buildExists": " ✓ 构建已存在",
"repoExists": " 仓库已存在,更新...",
"repoUpdateFailed": " 警告: 更新仓库失败",
"cloneRepoFailed": " 警告: 克隆仓库失败",
"skillMdExists": " ✓ SKILL.md 已存在",
"skillMdGenerated": " ✓ 生成 SKILL.md",
"skillMdFailed": " 警告: 生成 SKILL.md 失败",
"commandMdGenerated": " ✓ 生成 commands/{{name}}.md",
"commandMdFailed": " 警告: 生成 commands/{{name}}.md 失败",
"configUpdated": " 更新配置文件: {{path}}",
"configAlreadyExists": " 配置文件已包含该技能包",
"depsUpToDate": " ✓ 依赖已是最新",
"buildUpToDate": " ✓ 构建已是最新",
"detailSection": "详细说明",
"usageSection": "用法",
"commandDefault": "{{name}} 命令"
}

View File

@@ -0,0 +1,8 @@
{
"description": "列出已安装的技能包",
"extensionDescription": "列出已安装的插件/技能",
"noSkills": "📦 没有已安装的技能包",
"installHint": "使用以下命令安装技能包:",
"installedExtensions": "📦 已安装的扩展 ({{installed}}/{{total}}):",
"commands": "命令: {{commands}}"
}

View File

@@ -0,0 +1,18 @@
{
"description": "启动 MCP Server提供所有已安装扩展的 MCP 工具",
"options.inspector": "启动 MCP Inspector 进行交互式调试",
"inspectorStarting": "🔍 启动 MCP Inspector...",
"inspectorDebugCmd": " 调试命令: pnpm space mcp",
"inspectorFailed": "❌ 启动 MCP Inspector 失败: {{error}}",
"extensionDescription": "启动 MCP Server提供所有已安装扩展的 MCP 工具",
"scanning": "🔍 扫描已安装的扩展...",
"foundExtensions": " 发现 {{count}} 个扩展",
"checkingExport": " 检查 {{key}}: __mcp_server__={{hasMcpServer}}",
"containerSuccess": " ✅ 从容器获取 {{key}} 实例成功",
"containerFailed": " ⚠️ 从容器获取 {{key}} 失败: {{error}}",
"loadToolsFailed": " ⚠️ {{name}}: 加载 MCP 工具失败",
"noToolsFound": "❌ 没有找到任何 MCP 工具",
"noToolsHint": " 提示: 确保已安装支持 MCP 的扩展,并导出 mcpService 或 getMcpTools",
"toolsFound": "✅ 共发现 {{count}} 个 MCP 工具",
"serverStarted": "🚀 MCP Server 已启动,共 {{count}} 个工具"
}

View File

@@ -0,0 +1,13 @@
{
"description": "全局安装依赖并运行命令",
"options.name": "指定命令名称(默认从 source 自动提取)",
"noSource": "❌ 请指定要运行的依赖源",
"usage": " 用法: spaceflow x <source> [args...]",
"runFailed": "❌ 运行失败: {{error}}",
"extensionDescription": "执行插件提供的脚本命令",
"runningCommand": "▶️ 运行命令: {{command}}",
"npxExitCode": "npx {{package}} 退出码: {{code}}",
"commandNotInstalled": "命令 {{name}} 未安装",
"commandNotBuilt": "命令 {{name}} 未构建,缺少 dist/index.js",
"pluginNoExport": "插件 {{name}} 没有默认导出"
}

View File

@@ -0,0 +1,4 @@
{
"description": "生成配置文件的 JSON Schema",
"extensionDescription": "生成配置文件的 JSON Schema"
}

View File

@@ -0,0 +1,14 @@
{
"description": "初始化 spaceflow 配置文件",
"options.global": "生成全局配置到 ~/spaceflow/spaceflow.json合并本地 .env 和 spaceflow.json",
"setupFailed": "❌ 初始化失败: {{error}}",
"extensionDescription": "初始化 spaceflow 配置文件",
"dirCreated": "✅ 已创建目录: {{dir}}",
"configGenerated": "✅ 已生成配置文件: {{path}}",
"configExists": " 配置文件已存在: {{path}}",
"localConfigRead": "📖 已读取本地配置",
"globalConfigGenerated": "✅ 已生成全局配置: {{path}}",
"envConfigMerged": "📝 已合并环境变量配置:",
"envRead": "📖 已读取环境变量: {{path}}",
"envReadFailed": "⚠️ 无法读取 .env 文件: {{path}}"
}

View File

@@ -0,0 +1,18 @@
{
"description": "卸载技能包",
"options.global": "从全局目录 (~/.spaceflow) 卸载并清理各个编辑器的关联",
"noName": "❌ 请指定要卸载的技能包名称",
"uninstallFailed": "❌ 卸载失败: {{error}}",
"extensionDescription": "卸载已安装的插件/技能",
"uninstallingExtension": " 卸载 Extension: {{name}}",
"targetDir": " 目标目录: {{dir}}",
"extensionUninstallFailed": " 警告: Extension 卸载失败: {{name}}",
"uninstallingGlobal": "🗑️ 卸载技能包: {{name}} (全局)",
"uninstalling": "🗑️ 卸载技能包: {{name}}",
"foundDependency": " 找到依赖: {{key}} -> {{value}}",
"notRegistered": "技能包 \"{{name}}\" 未在配置文件中注册",
"uninstallDone": "✅ 卸载完成",
"deletingSkill": " 删除: skills/{{entry}}",
"deletingCommand": " 删除: commands/{{entry}}",
"configUpdated": " 更新配置文件: {{path}}"
}

View File

@@ -0,0 +1,28 @@
{
"description": "更新依赖npm 包获取最新版本git 仓库拉取最新代码)",
"options.self": "更新 spaceflow CLI 自身",
"updateFailed": "❌ 更新失败: {{error}}",
"extensionDescription": "更新依赖npm 包获取最新版本git 仓库拉取最新代码)",
"cannotGetLatest": " ⚠️ 无法获取 {{package}} 的最新版本",
"alreadyLatest": " ✓ {{package}} 已是最新版本 ({{version}})",
"versionChange": " {{package}}: {{current}} -> {{latest}}",
"unknown": "未知",
"npmUpdateFailed": " ❌ 更新 {{package}} 失败:",
"notGitRepo": " ⚠️ {{name}} 不是 git 仓库(可能已移除 .git",
"reinstallHint": " 提示: 使用 spaceflow install <source> 重新安装",
"pullingLatest": " 拉取最新代码...",
"gitUpdateFailed": " ❌ 更新 {{name}} 失败:",
"updatingCli": "🔄 更新 spaceflow CLI...",
"installMethod": " 安装方式: {{method}} ({{pm}})",
"installGlobal": "全局",
"installLocal": "本地",
"executing": " 执行: {{cmd}}",
"cliUpdateDone": "✅ CLI 更新完成",
"cliUpdateFailed": "❌ CLI 更新失败:",
"depNotFound": "❌ 未找到依赖: {{name}}",
"updating": "📦 更新: {{name}}",
"localNoUpdate": " ✓ 本地路径无需更新",
"noDeps": "📦 没有找到依赖",
"updatingAll": "🔄 更新所有依赖 ({{count}} 个)...",
"updateComplete": "✅ 更新完成: {{success}} 成功, {{fail}} 失败"
}

23
cli/tsconfig.json Normal file
View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false,
"resolveJsonModule": true
}
}

11
cli/vitest.config.ts Normal file
View File

@@ -0,0 +1,11 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
root: "src",
globals: true,
environment: "node",
include: ["**/*.spec.ts"],
passWithNoTests: true,
},
});