@simoonfish/df-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.d.ts +53 -0
- package/dist/api/client.js +81 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +59 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.js +85 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +39 -0
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +179 -0
- package/dist/commands/unbind.d.ts +2 -0
- package/dist/commands/unbind.js +28 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.js +105 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +31 -0
- package/package.json +34 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
declare class ApiClient {
|
|
2
|
+
private baseUrl;
|
|
3
|
+
private apiKey;
|
|
4
|
+
constructor();
|
|
5
|
+
/** 重置为环境变量/配置文件中的值 */
|
|
6
|
+
reset(): void;
|
|
7
|
+
/** 运行时覆盖(优先级高于环境变量/配置文件,用于 --api-key 等 CLI 参数) */
|
|
8
|
+
configure(opts: {
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
}): void;
|
|
12
|
+
private headers;
|
|
13
|
+
private url;
|
|
14
|
+
get<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T>;
|
|
15
|
+
post<T>(path: string, body?: unknown): Promise<T>;
|
|
16
|
+
verifyKey(): Promise<{
|
|
17
|
+
valid: boolean;
|
|
18
|
+
user_id: number | null;
|
|
19
|
+
}>;
|
|
20
|
+
getWorkspace(id: number): Promise<{
|
|
21
|
+
code: number;
|
|
22
|
+
data: any;
|
|
23
|
+
}>;
|
|
24
|
+
getWorkspaceSkills(wsId: number): Promise<{
|
|
25
|
+
code: number;
|
|
26
|
+
data: any[];
|
|
27
|
+
}>;
|
|
28
|
+
getWorkspaceKnowledgeBases(wsId: number): Promise<{
|
|
29
|
+
code: number;
|
|
30
|
+
data: any[];
|
|
31
|
+
}>;
|
|
32
|
+
getWorkspaceRule(wsId: number): Promise<{
|
|
33
|
+
code: number;
|
|
34
|
+
data: any;
|
|
35
|
+
}>;
|
|
36
|
+
getSkill(id: number): Promise<{
|
|
37
|
+
code: number;
|
|
38
|
+
data: any;
|
|
39
|
+
}>;
|
|
40
|
+
getKnowledgeNodes(baseId: number): Promise<{
|
|
41
|
+
code: number;
|
|
42
|
+
data: any[];
|
|
43
|
+
}>;
|
|
44
|
+
syncReport(data: {
|
|
45
|
+
workspace_id: number;
|
|
46
|
+
hostname?: string;
|
|
47
|
+
local_dir?: string;
|
|
48
|
+
action: string;
|
|
49
|
+
detail?: string;
|
|
50
|
+
}): Promise<unknown>;
|
|
51
|
+
}
|
|
52
|
+
export declare const api: ApiClient;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { readConfig } from "../config.js";
|
|
2
|
+
class ApiClient {
|
|
3
|
+
baseUrl;
|
|
4
|
+
apiKey;
|
|
5
|
+
constructor() {
|
|
6
|
+
this.reset();
|
|
7
|
+
}
|
|
8
|
+
/** 重置为环境变量/配置文件中的值 */
|
|
9
|
+
reset() {
|
|
10
|
+
const config = readConfig();
|
|
11
|
+
// 优先级: 环境变量 > 配置文件 > 默认值
|
|
12
|
+
this.baseUrl = process.env.DEVFORGE_API_URL || config?.api_url || "http://coding.bynway.com/api/v1";
|
|
13
|
+
this.apiKey = process.env.DEVFORGE_API_KEY || config?.api_key || "";
|
|
14
|
+
}
|
|
15
|
+
/** 运行时覆盖(优先级高于环境变量/配置文件,用于 --api-key 等 CLI 参数) */
|
|
16
|
+
configure(opts) {
|
|
17
|
+
if (opts.baseUrl)
|
|
18
|
+
this.baseUrl = opts.baseUrl;
|
|
19
|
+
if (opts.apiKey)
|
|
20
|
+
this.apiKey = opts.apiKey;
|
|
21
|
+
}
|
|
22
|
+
headers() {
|
|
23
|
+
return {
|
|
24
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
url(path, params) {
|
|
29
|
+
const u = new URL(`${this.baseUrl}${path}`);
|
|
30
|
+
if (params) {
|
|
31
|
+
Object.entries(params).forEach(([k, v]) => {
|
|
32
|
+
if (v !== undefined)
|
|
33
|
+
u.searchParams.set(k, String(v));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return u.toString();
|
|
37
|
+
}
|
|
38
|
+
async get(path, params) {
|
|
39
|
+
const res = await fetch(this.url(path, params), { headers: this.headers() });
|
|
40
|
+
if (!res.ok)
|
|
41
|
+
throw new Error(`API ${res.status}: ${res.statusText}`);
|
|
42
|
+
const json = await res.json();
|
|
43
|
+
return json;
|
|
44
|
+
}
|
|
45
|
+
async post(path, body) {
|
|
46
|
+
const res = await fetch(this.url(path), {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: this.headers(),
|
|
49
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
50
|
+
});
|
|
51
|
+
if (!res.ok)
|
|
52
|
+
throw new Error(`API ${res.status}: ${res.statusText}`);
|
|
53
|
+
const json = await res.json();
|
|
54
|
+
return json;
|
|
55
|
+
}
|
|
56
|
+
async verifyKey() {
|
|
57
|
+
return this.post("/auth/api-keys/verify");
|
|
58
|
+
}
|
|
59
|
+
async getWorkspace(id) {
|
|
60
|
+
return this.get(`/workspaces/${id}`);
|
|
61
|
+
}
|
|
62
|
+
async getWorkspaceSkills(wsId) {
|
|
63
|
+
return this.get(`/workspaces/${wsId}/skills`);
|
|
64
|
+
}
|
|
65
|
+
async getWorkspaceKnowledgeBases(wsId) {
|
|
66
|
+
return this.get(`/workspaces/${wsId}/knowledge-bases`);
|
|
67
|
+
}
|
|
68
|
+
async getWorkspaceRule(wsId) {
|
|
69
|
+
return this.get(`/workspaces/${wsId}/rule`);
|
|
70
|
+
}
|
|
71
|
+
async getSkill(id) {
|
|
72
|
+
return this.get(`/skills/${id}`);
|
|
73
|
+
}
|
|
74
|
+
async getKnowledgeNodes(baseId) {
|
|
75
|
+
return this.get("/knowledge-nodes", { base_id: baseId });
|
|
76
|
+
}
|
|
77
|
+
async syncReport(data) {
|
|
78
|
+
return this.post("/clients/sync-report", data);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export const api = new ApiClient();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { readConfig, writeConfig } from "../config.js";
|
|
5
|
+
import { api } from "../api/client.js";
|
|
6
|
+
export const initCommand = new Command("init")
|
|
7
|
+
.description("初始化 DevForge CLI 配置(API 地址 + Key)")
|
|
8
|
+
.option("--api-url <url>", "平台 API 地址")
|
|
9
|
+
.option("--api-key <key>", "平台 API Key")
|
|
10
|
+
.option("--platform <name>", "AI 编码平台: claude_code | codex")
|
|
11
|
+
.action(async (opts, cmd) => {
|
|
12
|
+
const spinner = ora("正在验证...").start();
|
|
13
|
+
try {
|
|
14
|
+
// Load existing config
|
|
15
|
+
let config = readConfig();
|
|
16
|
+
// 优先级: CLI 参数 > 环境变量 > 配置文件 > 默认值
|
|
17
|
+
const parentOpts = cmd.parent?.optsWithGlobals() || {};
|
|
18
|
+
const apiUrl = opts.apiUrl || parentOpts.apiUrl || process.env.DEVFORGE_API_URL || config?.api_url || "http://coding.bynway.com/api/v1";
|
|
19
|
+
let apiKey = opts.apiKey || parentOpts.apiKey || process.env.DEVFORGE_API_KEY || config?.api_key || "";
|
|
20
|
+
if (!apiKey) {
|
|
21
|
+
spinner.fail("请通过 --api-key 或环境变量 DEVFORGE_API_KEY 提供 API Key");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
// 将 CLI 参数同步到 API 客户端
|
|
25
|
+
api.configure({ baseUrl: apiUrl, apiKey });
|
|
26
|
+
// Verify key
|
|
27
|
+
spinner.text = "验证 API Key...";
|
|
28
|
+
const verifyResult = await api.verifyKey();
|
|
29
|
+
if (!verifyResult.valid) {
|
|
30
|
+
spinner.fail("API Key 无效,请检查后重试");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
spinner.succeed("API Key 验证通过");
|
|
34
|
+
// Save config (no bindings — just url, key, platform)
|
|
35
|
+
if (!config) {
|
|
36
|
+
config = {
|
|
37
|
+
version: 1,
|
|
38
|
+
api_url: apiUrl,
|
|
39
|
+
api_key: apiKey,
|
|
40
|
+
platform: opts.platform || undefined,
|
|
41
|
+
bindings: [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
config.api_url = apiUrl;
|
|
46
|
+
config.api_key = apiKey;
|
|
47
|
+
if (opts.platform)
|
|
48
|
+
config.platform = opts.platform;
|
|
49
|
+
}
|
|
50
|
+
writeConfig(config);
|
|
51
|
+
spinner.succeed("配置已保存到 ~/.devforge/config.json");
|
|
52
|
+
console.log(chalk.gray(` 平台地址: ${apiUrl}`));
|
|
53
|
+
console.log(chalk.gray(` 运行 ${chalk.white("devforge sync --workspace-id <id>")} 绑定工作区并同步`));
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
spinner.fail(`初始化失败: ${e.message}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { readConfig, findBinding, getPlatform, getSkillInstallDir, getKnowledgeInstallDir, SUPPORTED_PLATFORMS } from "../config.js";
|
|
7
|
+
import { api } from "../api/client.js";
|
|
8
|
+
export const installCommand = new Command("install")
|
|
9
|
+
.description("安装指定 Skill 或知识库到工作区")
|
|
10
|
+
.argument("<type>", '资源类型: skill | knowledge')
|
|
11
|
+
.argument("<name>", "资源名称")
|
|
12
|
+
.option("--local-dir <path>", "指定本地工程目录(默认当前目录)")
|
|
13
|
+
.option("--platform <name>", `目标 AI 编码平台 (${SUPPORTED_PLATFORMS.join(" | ")}),覆盖配置中的平台`)
|
|
14
|
+
.option("--force", "当 platform 不匹配时强制安装")
|
|
15
|
+
.action(async (type, name, opts) => {
|
|
16
|
+
const spinner = ora().start();
|
|
17
|
+
try {
|
|
18
|
+
const config = readConfig();
|
|
19
|
+
if (!config) {
|
|
20
|
+
spinner.fail("未找到配置,请先运行 devforge init");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const binding = findBinding(config, opts.localDir);
|
|
24
|
+
if (!binding) {
|
|
25
|
+
spinner.fail("当前目录未绑定工作区,请运行 devforge init");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
if (type === "skill") {
|
|
29
|
+
await installSkill(config, binding, name, opts);
|
|
30
|
+
}
|
|
31
|
+
else if (type === "knowledge") {
|
|
32
|
+
await installKnowledgeBase(config, binding, name, opts);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
spinner.fail(`未知资源类型: ${type} (支持: skill | knowledge)`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
spinner.fail(`安装失败: ${e.message}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
async function installSkill(config, binding, name, opts) {
|
|
45
|
+
const spinner = ora().start();
|
|
46
|
+
// Search skill by name
|
|
47
|
+
spinner.text = `搜索 Skill "${name}"...`;
|
|
48
|
+
const res = await api.get("/skills", { keyword: name, pageSize: 20 });
|
|
49
|
+
const skills = res.data || [];
|
|
50
|
+
const skill = skills.find((s) => s.name === name || s.name.toLowerCase() === name.toLowerCase());
|
|
51
|
+
if (!skill) {
|
|
52
|
+
spinner.fail(`未找到名为 "${name}" 的 Skill`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
// Platform compatibility check
|
|
56
|
+
const currentPlatform = getPlatform(opts.platform);
|
|
57
|
+
const skillPlatform = skill.platform || "claude_code";
|
|
58
|
+
if (skillPlatform !== currentPlatform && !opts.force) {
|
|
59
|
+
spinner.warn(`Skill "${skill.name}" 的目标平台是 ${chalk.cyan(skillPlatform)},` +
|
|
60
|
+
`当前环境是 ${chalk.cyan(currentPlatform)}`);
|
|
61
|
+
console.log(chalk.yellow(` 使用 ${chalk.white("--platform " + skillPlatform)} 切换平台`));
|
|
62
|
+
console.log(chalk.yellow(` 或使用 ${chalk.white("--force")} 强制安装`));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
// Fetch full detail
|
|
66
|
+
spinner.text = `获取 Skill "${skill.name}" 详情...`;
|
|
67
|
+
const full = await api.getSkill(skill.id);
|
|
68
|
+
const detail = full.data || full;
|
|
69
|
+
const baseDir = getSkillInstallDir(binding.local_dir, skill.scope || "project", skill.platform);
|
|
70
|
+
const skillDir = join(baseDir, detail.name);
|
|
71
|
+
mkdirSync(skillDir, { recursive: true });
|
|
72
|
+
mkdirSync(join(skillDir, "files"), { recursive: true });
|
|
73
|
+
// Write manifest
|
|
74
|
+
writeFileSync(join(skillDir, "manifest.json"), JSON.stringify({
|
|
75
|
+
id: detail.id, name: detail.name, version: detail.version,
|
|
76
|
+
scope: detail.scope, status: detail.status,
|
|
77
|
+
platform: detail.platform, description: detail.description,
|
|
78
|
+
}, null, 2));
|
|
79
|
+
// Write prompt
|
|
80
|
+
if (detail.system_prompt) {
|
|
81
|
+
writeFileSync(join(skillDir, "SKILL.md"), detail.system_prompt);
|
|
82
|
+
}
|
|
83
|
+
// Download files
|
|
84
|
+
if (detail.files?.length > 0) {
|
|
85
|
+
for (const f of detail.files) {
|
|
86
|
+
spinner.text = ` 下载附件 ${f.file_name}...`;
|
|
87
|
+
const blob = await fetch(`${config.api_url}/skills/${detail.id}/files/${f.id}/download`, { headers: { Authorization: `Bearer ${config.api_key}` } });
|
|
88
|
+
if (blob.ok) {
|
|
89
|
+
const buf = Buffer.from(await blob.arrayBuffer());
|
|
90
|
+
writeFileSync(join(skillDir, "files", f.file_name), buf);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
spinner.succeed(`Skill "${skill.name}" 安装成功`);
|
|
95
|
+
console.log(chalk.gray(` 目录: ${skillDir}`));
|
|
96
|
+
console.log(chalk.gray(` 平台: ${chalk.cyan(skillPlatform)}`));
|
|
97
|
+
}
|
|
98
|
+
async function installKnowledgeBase(config, binding, name, _opts) {
|
|
99
|
+
const spinner = ora().start();
|
|
100
|
+
spinner.text = `搜索知识库 "${name}"...`;
|
|
101
|
+
const res = await api.get("/knowledge-bases", { keyword: name, pageSize: 20 });
|
|
102
|
+
const kbs = res.data || [];
|
|
103
|
+
const kb = kbs.find((k) => k.name === name || k.name.toLowerCase() === name.toLowerCase());
|
|
104
|
+
if (!kb) {
|
|
105
|
+
spinner.fail(`未找到名为 "${name}" 的知识库`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
const kbRootDir = getKnowledgeInstallDir(binding.local_dir);
|
|
109
|
+
mkdirSync(kbRootDir, { recursive: true });
|
|
110
|
+
const kbDir = join(kbRootDir, kb.name);
|
|
111
|
+
mkdirSync(kbDir, { recursive: true });
|
|
112
|
+
writeFileSync(join(kbDir, "manifest.json"), JSON.stringify({ id: kb.id, name: kb.name, path_in_repo: kb.path_in_repo }, null, 2));
|
|
113
|
+
const nodesRes = await api.get("/knowledge-nodes", { base_id: kb.id });
|
|
114
|
+
const nodes = nodesRes.data || [];
|
|
115
|
+
writeFileSync(join(kbDir, "index.json"), JSON.stringify(nodes.map((n) => ({
|
|
116
|
+
id: n.id, parent_id: n.parent_id, title: n.title,
|
|
117
|
+
type: n.type, path: n.path, sort_order: n.sort_order,
|
|
118
|
+
})), null, 2));
|
|
119
|
+
for (const node of nodes) {
|
|
120
|
+
if (node.type === "document" && node.content) {
|
|
121
|
+
const relPath = node.path || `${node.title}.md`;
|
|
122
|
+
const filePath = join(kbDir, relPath);
|
|
123
|
+
mkdirSync(join(filePath, ".."), { recursive: true });
|
|
124
|
+
writeFileSync(filePath, node.content);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
spinner.succeed(`知识库 "${kb.name}" 安装成功`);
|
|
128
|
+
console.log(chalk.gray(` 目录: ${kbDir}`));
|
|
129
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { readConfig, findBinding, getAllSkillDirs, getKnowledgeInstallDir } from "../config.js";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
export const listCommand = new Command("list")
|
|
7
|
+
.description("列出已安装的 Skill 和知识库")
|
|
8
|
+
.option("--local-dir <path>", "指定本地工程目录(默认当前目录)")
|
|
9
|
+
.action((opts) => {
|
|
10
|
+
try {
|
|
11
|
+
const config = readConfig();
|
|
12
|
+
if (!config) {
|
|
13
|
+
console.log(chalk.yellow("未找到配置,请先运行 devforge init"));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const localDir = opts.localDir || process.cwd();
|
|
17
|
+
const binding = findBinding(config, localDir);
|
|
18
|
+
if (!binding) {
|
|
19
|
+
console.log(chalk.yellow("当前目录未绑定工作区"));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const platformLabel = {
|
|
23
|
+
claude_code: "Claude Code", codex: "Codex", qoder: "Qoder", trae: "Trae",
|
|
24
|
+
};
|
|
25
|
+
console.log(chalk.bold(`\n工作区: ${binding.workspace_name} (ID: ${binding.workspace_id})\n`));
|
|
26
|
+
// ── Skills: 扫描所有可能的安装目录 ──────────────────────────
|
|
27
|
+
console.log(chalk.cyan("Skills:"));
|
|
28
|
+
const seenSkills = new Set();
|
|
29
|
+
let hasSkill = false;
|
|
30
|
+
for (const skillDir of getAllSkillDirs(localDir)) {
|
|
31
|
+
if (!existsSync(skillDir))
|
|
32
|
+
continue;
|
|
33
|
+
const entries = readdirSync(skillDir, { withFileTypes: true });
|
|
34
|
+
const folders = entries.filter((e) => e.isDirectory());
|
|
35
|
+
for (const f of folders) {
|
|
36
|
+
try {
|
|
37
|
+
const m = JSON.parse(readFileSync(join(skillDir, f.name, "manifest.json"), "utf-8"));
|
|
38
|
+
const key = `${m.name}:${m.platform}:${m.scope}`;
|
|
39
|
+
if (seenSkills.has(key))
|
|
40
|
+
continue;
|
|
41
|
+
seenSkills.add(key);
|
|
42
|
+
hasSkill = true;
|
|
43
|
+
const plat = platformLabel[m.platform] || m.platform || "";
|
|
44
|
+
const scopeLabel = { project: "项目级", team: "团队级", enterprise: "企业级" }[m.scope] || m.scope;
|
|
45
|
+
console.log(` ${chalk.white(m.name)} ${chalk.gray(`v${m.version}`)} ${chalk.gray(scopeLabel)} ${plat ? chalk.cyan(plat) : ""}`);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// invalid manifest, skip
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!hasSkill)
|
|
53
|
+
console.log(chalk.gray(" (无)"));
|
|
54
|
+
// ── Knowledge bases ────────────────────────────────────────
|
|
55
|
+
console.log(chalk.cyan("\n知识库:"));
|
|
56
|
+
const kbDir = getKnowledgeInstallDir(localDir);
|
|
57
|
+
if (existsSync(kbDir)) {
|
|
58
|
+
const entries = readdirSync(kbDir, { withFileTypes: true });
|
|
59
|
+
const folders = entries.filter((e) => e.isDirectory());
|
|
60
|
+
if (folders.length > 0) {
|
|
61
|
+
folders.forEach((f) => {
|
|
62
|
+
try {
|
|
63
|
+
const m = JSON.parse(readFileSync(join(kbDir, f.name, "manifest.json"), "utf-8"));
|
|
64
|
+
const idx = JSON.parse(readFileSync(join(kbDir, f.name, "index.json"), "utf-8"));
|
|
65
|
+
const docCount = idx.filter((n) => n.type === "document").length;
|
|
66
|
+
console.log(` ${chalk.white(m.name)} ${chalk.gray(`${docCount} 篇文档`)}`);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
console.log(` ${f.name}`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.log(chalk.gray(" (无)"));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.log(chalk.gray(" (无)"));
|
|
79
|
+
}
|
|
80
|
+
console.log("");
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
console.error(chalk.red(`错误: ${e.message}`));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { readConfig, findBinding } from "../config.js";
|
|
4
|
+
export const statusCommand = new Command("status")
|
|
5
|
+
.description("查看当前工程绑定状态")
|
|
6
|
+
.option("--local-dir <path>", "指定本地工程目录(默认当前目录)")
|
|
7
|
+
.action((opts) => {
|
|
8
|
+
try {
|
|
9
|
+
const config = readConfig();
|
|
10
|
+
if (!config) {
|
|
11
|
+
console.log(chalk.yellow("未找到配置,请先运行 devforge init"));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
console.log(chalk.bold("\nDevForge CLI 状态\n"));
|
|
15
|
+
console.log(`平台地址: ${chalk.blue(config.api_url)}`);
|
|
16
|
+
if (config.bindings.length === 0) {
|
|
17
|
+
console.log(chalk.yellow("暂无绑定"));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const currentBinding = findBinding(config, opts.localDir);
|
|
21
|
+
if (currentBinding) {
|
|
22
|
+
console.log(`当前目录: ${chalk.white(currentBinding.local_dir)}`);
|
|
23
|
+
console.log(`绑定工作区: ${chalk.green(currentBinding.workspace_name)} (ID: ${currentBinding.workspace_id})`);
|
|
24
|
+
console.log(`最后同步: ${currentBinding.last_synced || "从未同步"}`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.log(chalk.gray("当前目录未绑定工作区"));
|
|
28
|
+
}
|
|
29
|
+
console.log(chalk.cyan("\n所有绑定:"));
|
|
30
|
+
config.bindings.forEach((b) => {
|
|
31
|
+
const marker = b.local_dir === (opts.localDir || process.cwd()) ? chalk.green(" ▶") : " ";
|
|
32
|
+
console.log(`${marker} ${chalk.white(b.local_dir)} → ${b.workspace_name} (ID:${b.workspace_id}) ${chalk.gray(b.last_synced || "未同步")}`);
|
|
33
|
+
});
|
|
34
|
+
console.log("");
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
console.error(chalk.red(`错误: ${e.message}`));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { readConfig, writeConfig, findBinding, getSkillInstallDir, getKnowledgeInstallDir, getDevForgeDir, getRuleFileName, getRulesCacheDir } from "../config.js";
|
|
7
|
+
import { api } from "../api/client.js";
|
|
8
|
+
export const syncCommand = new Command("sync")
|
|
9
|
+
.description("绑定工作区并同步 Skill + 知识库到本地")
|
|
10
|
+
.option("--local-dir <path>", "指定本地工程目录(默认当前目录)")
|
|
11
|
+
.option("--workspace-id <id>", "工作区 ID(首次绑定时必填)")
|
|
12
|
+
.option("--workspace <name>", "工作区名称(首次绑定时按名称搜索)")
|
|
13
|
+
.option("--skills", "仅同步 Skill")
|
|
14
|
+
.option("--knowledge", "仅同步知识库")
|
|
15
|
+
.action(async (opts) => {
|
|
16
|
+
const spinner = ora("加载配置...").start();
|
|
17
|
+
try {
|
|
18
|
+
const config = readConfig();
|
|
19
|
+
if (!config) {
|
|
20
|
+
spinner.fail("未找到配置,请先运行 devforge init");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const localDir = opts.localDir || process.cwd();
|
|
24
|
+
let binding = findBinding(config, localDir);
|
|
25
|
+
// ── 首次绑定 ─────────────────────────────────────────────
|
|
26
|
+
if (!binding) {
|
|
27
|
+
let wsId;
|
|
28
|
+
let wsName;
|
|
29
|
+
const workspaceId = opts.workspaceId ? Number(opts.workspaceId) : undefined;
|
|
30
|
+
const workspaceName = opts.workspace;
|
|
31
|
+
if (workspaceId) {
|
|
32
|
+
spinner.text = "查询工作区...";
|
|
33
|
+
const ws = await api.getWorkspace(workspaceId);
|
|
34
|
+
wsName = ws.data?.name || `工作区 #${workspaceId}`;
|
|
35
|
+
wsId = workspaceId;
|
|
36
|
+
}
|
|
37
|
+
else if (workspaceName) {
|
|
38
|
+
spinner.text = `搜索工作区 "${workspaceName}"...`;
|
|
39
|
+
const res = await api.get(`/workspaces?keyword=${encodeURIComponent(workspaceName)}&pageSize=5`);
|
|
40
|
+
if (!res.data?.length) {
|
|
41
|
+
spinner.fail(`未找到名为 "${workspaceName}" 的工作区`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
wsId = res.data[0].id;
|
|
45
|
+
wsName = res.data[0].name;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
spinner.fail("当前目录未绑定工作区,请通过 --workspace-id 或 --workspace 指定");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
binding = { local_dir: localDir, workspace_id: wsId, workspace_name: wsName };
|
|
52
|
+
config.bindings.push(binding);
|
|
53
|
+
writeConfig(config);
|
|
54
|
+
spinner.succeed(`已绑定工作区: ${wsName} (ID: ${wsId})`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
spinner.succeed(`已定位工作区: ${binding.workspace_name} (ID: ${binding.workspace_id})`);
|
|
58
|
+
}
|
|
59
|
+
// ── 同步到 local-dir/.devforge ────────────────────────────
|
|
60
|
+
mkdirSync(getDevForgeDir(binding.local_dir), { recursive: true });
|
|
61
|
+
const syncAll = !opts.skills && !opts.knowledge;
|
|
62
|
+
let skillCount = 0;
|
|
63
|
+
let kbCount = 0;
|
|
64
|
+
// Sync Skills — 按 scope + platform 安装到对应目录
|
|
65
|
+
if (syncAll || opts.skills) {
|
|
66
|
+
spinner.start(`同步 Skill... (工作区: ${binding.workspace_name})`);
|
|
67
|
+
const res = await api.getWorkspaceSkills(binding.workspace_id);
|
|
68
|
+
const skills = res.data || [];
|
|
69
|
+
for (const skill of skills) {
|
|
70
|
+
const baseDir = getSkillInstallDir(localDir, skill.scope || "project", skill.platform);
|
|
71
|
+
const skillDir = join(baseDir, skill.name);
|
|
72
|
+
mkdirSync(skillDir, { recursive: true });
|
|
73
|
+
mkdirSync(join(skillDir, "files"), { recursive: true });
|
|
74
|
+
// Fetch full skill with files
|
|
75
|
+
const full = await api.getSkill(skill.id);
|
|
76
|
+
const detail = full.data || full;
|
|
77
|
+
writeFileSync(join(skillDir, "manifest.json"), JSON.stringify({
|
|
78
|
+
id: detail.id, name: detail.name, version: detail.version,
|
|
79
|
+
scope: detail.scope, status: detail.status,
|
|
80
|
+
platform: detail.platform, description: detail.description,
|
|
81
|
+
}, null, 2));
|
|
82
|
+
if (detail.system_prompt) {
|
|
83
|
+
writeFileSync(join(skillDir, "SKILL.md"), detail.system_prompt);
|
|
84
|
+
}
|
|
85
|
+
if (detail.files?.length > 0) {
|
|
86
|
+
for (const f of detail.files) {
|
|
87
|
+
spinner.text = ` 下载 ${f.file_name}...`;
|
|
88
|
+
const blob = await fetch(`${config.api_url}/skills/${detail.id}/files/${f.id}/download`, { headers: { Authorization: `Bearer ${config.api_key}` } });
|
|
89
|
+
if (blob.ok) {
|
|
90
|
+
const buf = Buffer.from(await blob.arrayBuffer());
|
|
91
|
+
writeFileSync(join(skillDir, "files", f.file_name), buf);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
skillCount++;
|
|
96
|
+
spinner.text = ` ✓ ${skill.name} → ${skillDir}`;
|
|
97
|
+
}
|
|
98
|
+
spinner.succeed(`Skill 同步完成 (${skillCount} 个)`);
|
|
99
|
+
}
|
|
100
|
+
// Sync Knowledge bases — 统一安装到 local-dir/.devforge/knowledge/
|
|
101
|
+
if (syncAll || opts.knowledge) {
|
|
102
|
+
spinner.start(`同步知识库... (工作区: ${binding.workspace_name})`);
|
|
103
|
+
const res = await api.getWorkspaceKnowledgeBases(binding.workspace_id);
|
|
104
|
+
const kbs = res.data || [];
|
|
105
|
+
const kbRootDir = getKnowledgeInstallDir(localDir);
|
|
106
|
+
mkdirSync(kbRootDir, { recursive: true });
|
|
107
|
+
for (const kb of kbs) {
|
|
108
|
+
const kbDir = join(kbRootDir, kb.name);
|
|
109
|
+
mkdirSync(kbDir, { recursive: true });
|
|
110
|
+
writeFileSync(join(kbDir, "manifest.json"), JSON.stringify({ id: kb.id, name: kb.name, path_in_repo: kb.path_in_repo }, null, 2));
|
|
111
|
+
const nodesRes = await api.getKnowledgeNodes(kb.id);
|
|
112
|
+
const nodes = nodesRes.data || [];
|
|
113
|
+
writeFileSync(join(kbDir, "index.json"), JSON.stringify(nodes.map((n) => ({
|
|
114
|
+
id: n.id, parent_id: n.parent_id, title: n.title,
|
|
115
|
+
type: n.type, path: n.path, sort_order: n.sort_order,
|
|
116
|
+
})), null, 2));
|
|
117
|
+
for (const node of nodes) {
|
|
118
|
+
if (node.type === "document" && node.content) {
|
|
119
|
+
const relPath = node.path || `${node.title}.md`;
|
|
120
|
+
const filePath = join(kbDir, relPath);
|
|
121
|
+
mkdirSync(join(filePath, ".."), { recursive: true });
|
|
122
|
+
writeFileSync(filePath, node.content);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
kbCount++;
|
|
126
|
+
}
|
|
127
|
+
spinner.succeed(`知识库同步完成 (${kbCount} 个)`);
|
|
128
|
+
}
|
|
129
|
+
let ruleCount = 0;
|
|
130
|
+
// Sync Rule — 写入工程根目录的 CLAUDE.md / .cursorrules
|
|
131
|
+
if (syncAll || opts.rules) {
|
|
132
|
+
spinner.start(`同步 AI 编码规则...`);
|
|
133
|
+
try {
|
|
134
|
+
const res = await api.getWorkspaceRule(binding.workspace_id);
|
|
135
|
+
const rule = res.data;
|
|
136
|
+
if (rule?.content) {
|
|
137
|
+
const platform = config.platform || "claude_code";
|
|
138
|
+
const fileName = getRuleFileName(platform);
|
|
139
|
+
const rulePath = join(localDir, fileName);
|
|
140
|
+
writeFileSync(rulePath, rule.content, "utf-8");
|
|
141
|
+
ruleCount = 1;
|
|
142
|
+
spinner.succeed(`规则已写入: ${rulePath}`);
|
|
143
|
+
// 缓存元数据到 .devforge/rules/
|
|
144
|
+
const cacheDir = getRulesCacheDir(localDir);
|
|
145
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
146
|
+
writeFileSync(join(cacheDir, "manifest.json"), JSON.stringify({
|
|
147
|
+
rule_id: rule.id,
|
|
148
|
+
version: rule.version,
|
|
149
|
+
template_name: rule.template_name || null,
|
|
150
|
+
synced_at: new Date().toISOString(),
|
|
151
|
+
knowledge_refs: rule.knowledge_refs || [],
|
|
152
|
+
}, null, 2));
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
spinner.info("工作区未配置 AI 编码规则,跳过");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
spinner.warn(`规则同步失败: ${e.message},跳过`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Update config
|
|
163
|
+
binding.last_synced = new Date().toISOString();
|
|
164
|
+
writeConfig(config);
|
|
165
|
+
// Report sync
|
|
166
|
+
await api.syncReport({
|
|
167
|
+
workspace_id: binding.workspace_id,
|
|
168
|
+
local_dir: binding.local_dir,
|
|
169
|
+
action: "sync",
|
|
170
|
+
detail: JSON.stringify({ skills: skillCount, knowledge: kbCount, rules: ruleCount }),
|
|
171
|
+
});
|
|
172
|
+
console.log(chalk.gray(` 工作区: ${binding.workspace_name} (ID: ${binding.workspace_id})`));
|
|
173
|
+
console.log(chalk.green(` 同步完成 — Skill ${skillCount}, 知识库 ${kbCount}`));
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
spinner.fail(`同步失败: ${e.message}`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { readConfig, writeConfig } from "../config.js";
|
|
4
|
+
export const unbindCommand = new Command("unbind")
|
|
5
|
+
.description("解绑当前目录或指定目录")
|
|
6
|
+
.option("--local-dir <path>", "指定本地工程目录(默认当前目录)")
|
|
7
|
+
.action((opts) => {
|
|
8
|
+
try {
|
|
9
|
+
const config = readConfig();
|
|
10
|
+
if (!config) {
|
|
11
|
+
console.log(chalk.yellow("未找到配置"));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const dir = opts.localDir || process.cwd();
|
|
15
|
+
const idx = config.bindings.findIndex((b) => b.local_dir === dir);
|
|
16
|
+
if (idx === -1) {
|
|
17
|
+
console.log(chalk.yellow(`目录 ${dir} 未绑定工作区`));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const binding = config.bindings[idx];
|
|
21
|
+
config.bindings.splice(idx, 1);
|
|
22
|
+
writeConfig(config);
|
|
23
|
+
console.log(chalk.green(`已解绑: ${binding.workspace_name} ← ${dir}`));
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
console.error(chalk.red(`错误: ${e.message}`));
|
|
27
|
+
}
|
|
28
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface Binding {
|
|
2
|
+
local_dir: string;
|
|
3
|
+
workspace_id: number;
|
|
4
|
+
workspace_name: string;
|
|
5
|
+
last_synced?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DevForgeConfig {
|
|
8
|
+
version: number;
|
|
9
|
+
api_url: string;
|
|
10
|
+
api_key: string;
|
|
11
|
+
platform?: string;
|
|
12
|
+
bindings: Binding[];
|
|
13
|
+
}
|
|
14
|
+
export declare const SUPPORTED_PLATFORMS: readonly ["claude_code", "codex"];
|
|
15
|
+
export type SupportedPlatform = (typeof SUPPORTED_PLATFORMS)[number];
|
|
16
|
+
export declare function ensureConfigDir(): void;
|
|
17
|
+
export declare function readConfig(): DevForgeConfig | null;
|
|
18
|
+
export declare function writeConfig(config: DevForgeConfig): void;
|
|
19
|
+
/** 确定当前运行的 AI 编码平台。优先级: 显式传入 > 配置文件 > 环境变量 > 默认 claude_code */
|
|
20
|
+
export declare function getPlatform(override?: string): string;
|
|
21
|
+
export declare function findBinding(config: DevForgeConfig, localDir?: string): Binding | null;
|
|
22
|
+
export declare function getWorkspaceDir(workspaceId: number): string;
|
|
23
|
+
export declare function ensureWorkspaceDir(workspaceId: number): string;
|
|
24
|
+
/** 根据 Skill 的 scope + platform 计算安装目录 */
|
|
25
|
+
export declare function getSkillInstallDir(localDir: string, scope: string, platform: string): string;
|
|
26
|
+
/** 知识库安装目录(统一在 local-dir/.devforge/knowledge/) */
|
|
27
|
+
export declare function getKnowledgeInstallDir(localDir: string): string;
|
|
28
|
+
/** 扫描所有可能的 skill 安装目录,返回存在的目录列表 */
|
|
29
|
+
export declare function getAllSkillDirs(localDir: string): string[];
|
|
30
|
+
/** 返回 {localDir}/.devforge 根目录 */
|
|
31
|
+
export declare function getDevForgeDir(localDir: string): string;
|
|
32
|
+
export declare const RULE_FILE_NAMES: Record<string, string>;
|
|
33
|
+
/** 根据 platform 返回规则文件名,默认 CLAUDE.md */
|
|
34
|
+
export declare function getRuleFileName(platform: string): string;
|
|
35
|
+
/** 规则元数据缓存目录 */
|
|
36
|
+
export declare function getRulesCacheDir(localDir: string): string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".devforge");
|
|
5
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
6
|
+
export const SUPPORTED_PLATFORMS = ["claude_code", "codex"];
|
|
7
|
+
export function ensureConfigDir() {
|
|
8
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
9
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function readConfig() {
|
|
13
|
+
if (!existsSync(CONFIG_FILE))
|
|
14
|
+
return null;
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function writeConfig(config) {
|
|
23
|
+
ensureConfigDir();
|
|
24
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
|
25
|
+
}
|
|
26
|
+
/** 确定当前运行的 AI 编码平台。优先级: 显式传入 > 配置文件 > 环境变量 > 默认 claude_code */
|
|
27
|
+
export function getPlatform(override) {
|
|
28
|
+
if (override && SUPPORTED_PLATFORMS.includes(override)) {
|
|
29
|
+
return override;
|
|
30
|
+
}
|
|
31
|
+
const config = readConfig();
|
|
32
|
+
if (config?.platform && SUPPORTED_PLATFORMS.includes(config.platform)) {
|
|
33
|
+
return config.platform;
|
|
34
|
+
}
|
|
35
|
+
const env = process.env.DEVFORGE_PLATFORM;
|
|
36
|
+
if (env && SUPPORTED_PLATFORMS.includes(env)) {
|
|
37
|
+
return env;
|
|
38
|
+
}
|
|
39
|
+
return "claude_code";
|
|
40
|
+
}
|
|
41
|
+
export function findBinding(config, localDir) {
|
|
42
|
+
const dir = localDir || process.cwd();
|
|
43
|
+
return config.bindings.find((b) => b.local_dir === dir) || null;
|
|
44
|
+
}
|
|
45
|
+
export function getWorkspaceDir(workspaceId) {
|
|
46
|
+
return join(CONFIG_DIR, "workspaces", String(workspaceId));
|
|
47
|
+
}
|
|
48
|
+
export function ensureWorkspaceDir(workspaceId) {
|
|
49
|
+
const dir = getWorkspaceDir(workspaceId);
|
|
50
|
+
mkdirSync(dir, { recursive: true });
|
|
51
|
+
return dir;
|
|
52
|
+
}
|
|
53
|
+
/** 根据 Skill 的 scope + platform 计算安装目录 */
|
|
54
|
+
export function getSkillInstallDir(localDir, scope, platform) {
|
|
55
|
+
const plat = platform || "claude_code";
|
|
56
|
+
const home = homedir();
|
|
57
|
+
if (scope === "project") {
|
|
58
|
+
// 项目级:安装到工程目录下
|
|
59
|
+
if (plat === "claude_code")
|
|
60
|
+
return join(localDir, ".claude", "skills");
|
|
61
|
+
if (plat === "codex")
|
|
62
|
+
return join(localDir, ".codex", "skills");
|
|
63
|
+
return join(localDir, ".claude", "skills"); // fallback
|
|
64
|
+
}
|
|
65
|
+
// team / enterprise → 用户级目录
|
|
66
|
+
if (plat === "claude_code")
|
|
67
|
+
return join(home, ".claude", "skills");
|
|
68
|
+
if (plat === "codex")
|
|
69
|
+
return join(home, ".codex", "skills");
|
|
70
|
+
return join(home, ".claude", "skills"); // fallback
|
|
71
|
+
}
|
|
72
|
+
/** 知识库安装目录(统一在 local-dir/.devforge/knowledge/) */
|
|
73
|
+
export function getKnowledgeInstallDir(localDir) {
|
|
74
|
+
return join(localDir, ".devforge", "knowledge");
|
|
75
|
+
}
|
|
76
|
+
/** 扫描所有可能的 skill 安装目录,返回存在的目录列表 */
|
|
77
|
+
export function getAllSkillDirs(localDir) {
|
|
78
|
+
const home = homedir();
|
|
79
|
+
return [
|
|
80
|
+
join(localDir, ".claude", "skills"),
|
|
81
|
+
join(localDir, ".codex", "skills"),
|
|
82
|
+
join(home, ".claude", "skills"),
|
|
83
|
+
join(home, ".codex", "skills"),
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
/** 返回 {localDir}/.devforge 根目录 */
|
|
87
|
+
export function getDevForgeDir(localDir) {
|
|
88
|
+
return join(localDir, ".devforge");
|
|
89
|
+
}
|
|
90
|
+
// ── Rule file names per platform ──────────────────────────────
|
|
91
|
+
export const RULE_FILE_NAMES = {
|
|
92
|
+
claude_code: "CLAUDE.md",
|
|
93
|
+
cursor: ".cursorrules",
|
|
94
|
+
codex: "AGENTS.md",
|
|
95
|
+
windsurf: ".windsurfrules",
|
|
96
|
+
cline: ".clinerules",
|
|
97
|
+
};
|
|
98
|
+
/** 根据 platform 返回规则文件名,默认 CLAUDE.md */
|
|
99
|
+
export function getRuleFileName(platform) {
|
|
100
|
+
return RULE_FILE_NAMES[platform] || "CLAUDE.md";
|
|
101
|
+
}
|
|
102
|
+
/** 规则元数据缓存目录 */
|
|
103
|
+
export function getRulesCacheDir(localDir) {
|
|
104
|
+
return join(localDir, ".devforge", "rules");
|
|
105
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { initCommand } from "./commands/init.js";
|
|
4
|
+
import { syncCommand } from "./commands/sync.js";
|
|
5
|
+
import { installCommand } from "./commands/install.js";
|
|
6
|
+
import { listCommand } from "./commands/list.js";
|
|
7
|
+
import { statusCommand } from "./commands/status.js";
|
|
8
|
+
import { unbindCommand } from "./commands/unbind.js";
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name("devforge")
|
|
12
|
+
.description("DevForge CLI — 将平台工作区、Skill 和知识库同步到本地开发工程")
|
|
13
|
+
.version("1.0.0")
|
|
14
|
+
.option("--api-url <url>", "平台 API 地址")
|
|
15
|
+
.option("--api-key <key>", "平台 API Key")
|
|
16
|
+
.option("--config <path>", "配置文件路径")
|
|
17
|
+
.addCommand(initCommand)
|
|
18
|
+
.addCommand(syncCommand)
|
|
19
|
+
.addCommand(installCommand)
|
|
20
|
+
.addCommand(listCommand)
|
|
21
|
+
.addCommand(statusCommand)
|
|
22
|
+
.addCommand(unbindCommand)
|
|
23
|
+
.hook("preAction", (thisCommand) => {
|
|
24
|
+
const opts = thisCommand.opts();
|
|
25
|
+
// require api-key for most commands except version/help
|
|
26
|
+
const cmdName = thisCommand.args[0];
|
|
27
|
+
const publicCommands = ["-V", "--version", "-h", "--help", "help"];
|
|
28
|
+
if (publicCommands.includes(cmdName))
|
|
29
|
+
return;
|
|
30
|
+
});
|
|
31
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@simoonfish/df-cli",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"description": "DevForge CLI — 将平台工作区、Skill 和知识库同步到本地开发工程",
|
|
8
|
+
"bin": {
|
|
9
|
+
"devforge": "./dist/index.js",
|
|
10
|
+
"df": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"commander": "^13.0.0",
|
|
23
|
+
"chalk": "^5.4.0",
|
|
24
|
+
"ora": "^8.2.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.0.0",
|
|
28
|
+
"typescript": "^5.7.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT"
|
|
34
|
+
}
|