@tofrankie/agents-sync 0.0.1
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/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.local.md +218 -0
- package/README.md +5 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +361 -0
- package/dist/index.d.mts +21 -0
- package/dist/index.mjs +6 -0
- package/package.json +88 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Frankie
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.local.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# @tofrankie/agents-sync (Local Draft)
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@tofrankie/agents-sync) [](https://nodejs.org) [](https://github.com/tofrankie/agents-sync/blob/main/LICENSE) [](https://www.npmjs.com/package/@tofrankie/agents-sync)
|
|
4
|
+
|
|
5
|
+
`agents-sync` 是一个用于同步 AI Agent 相关资源的 CLI 工具。当前版本聚焦于:
|
|
6
|
+
|
|
7
|
+
- `skill`:多 Agent 兼容同步(统一落到项目 `.agents/skills`)
|
|
8
|
+
- `rule`:首版仅支持 Cursor(写入 `.cursor/rules`)
|
|
9
|
+
- `mcp`:首版仅支持 Cursor(写入 `.cursor/mcp.json`)
|
|
10
|
+
|
|
11
|
+
> 当前文档是本地草稿,暂不对外发布。
|
|
12
|
+
|
|
13
|
+
## 1. 设计目标
|
|
14
|
+
|
|
15
|
+
- 从统一来源(默认 `~/.agents`)同步可复用的能力资产
|
|
16
|
+
- 在不同 Agent 生态之间,尽量用最少心智成本管理 skills
|
|
17
|
+
- 对差异较大的 rules / mcp,先收敛范围,确保首版稳定
|
|
18
|
+
|
|
19
|
+
## 2. 当前能力边界(v1)
|
|
20
|
+
|
|
21
|
+
- `skill`
|
|
22
|
+
- 支持目标 Agent:`cursor | claude | codex | gemini`
|
|
23
|
+
- 默认仍写入项目 `./.agents/skills/<skill-name>/`
|
|
24
|
+
- `rule`
|
|
25
|
+
- 仅支持目标 Agent:`cursor`
|
|
26
|
+
- 目标目录:`./.cursor/rules/`
|
|
27
|
+
- 源文件若为 `.md`,落盘会转为 `.mdc`
|
|
28
|
+
- `mcp`
|
|
29
|
+
- 仅支持目标 Agent:`cursor`
|
|
30
|
+
- 目标文件:`./.cursor/mcp.json`
|
|
31
|
+
- 对不支持的组合(如 `rule --target-agent codex`):
|
|
32
|
+
- 输出友好提示并跳过,不中断整个流程
|
|
33
|
+
|
|
34
|
+
## 3. 安装与运行
|
|
35
|
+
|
|
36
|
+
### 3.1 本地开发
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm install
|
|
40
|
+
pnpm dev
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 3.2 构建
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pnpm build
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
构建产物默认在 `dist/`,并通过 `bin` 暴露 `agents-sync` 命令。
|
|
50
|
+
|
|
51
|
+
## 4. 命令总览
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
agents-sync skill [options]
|
|
55
|
+
agents-sync rule [options]
|
|
56
|
+
agents-sync mcp [options]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 4.1 公共参数
|
|
60
|
+
|
|
61
|
+
- `-s, --source <path>`:源路径(默认 `~/.agents`)
|
|
62
|
+
- `-t, --target-agent <agent>`:目标 Agent(默认 `cursor`)
|
|
63
|
+
- `--scope <project|user>`:作用域(默认 `project`)
|
|
64
|
+
- `-c, --config <path>`:显式配置文件路径
|
|
65
|
+
- `--dry-run`:仅展示计划,不落盘
|
|
66
|
+
- `-y, --yes`:跳过交互,并在冲突时按覆盖策略执行
|
|
67
|
+
|
|
68
|
+
## 5. 使用示例
|
|
69
|
+
|
|
70
|
+
### 5.1 同步 skills(交互选择)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
agents-sync skill
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
行为:
|
|
77
|
+
|
|
78
|
+
- 从 `~/.agents` 发现 skill
|
|
79
|
+
- 交互多选
|
|
80
|
+
- 同步到 `./.agents/skills/`
|
|
81
|
+
|
|
82
|
+
### 5.2 同步 Cursor rules
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
agents-sync rule --target-agent cursor
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 5.3 同步 Cursor MCP(先预览)
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
agents-sync mcp --dry-run
|
|
92
|
+
agents-sync mcp
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 6. 配置文件
|
|
96
|
+
|
|
97
|
+
支持自动发现以下文件名:
|
|
98
|
+
|
|
99
|
+
- `agents-sync.config.js`
|
|
100
|
+
- `agents-sync.config.cjs`
|
|
101
|
+
- `agents-sync.config.mjs`
|
|
102
|
+
- `agents-sync.config.ts`
|
|
103
|
+
- `agents-sync.config.cts`
|
|
104
|
+
- `agents-sync.config.mts`
|
|
105
|
+
|
|
106
|
+
也可通过 `--config` 显式指定。
|
|
107
|
+
|
|
108
|
+
### 6.1 配置示例
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { defineConfig } from '@tofrankie/agents-sync'
|
|
112
|
+
|
|
113
|
+
export default defineConfig({
|
|
114
|
+
sourceDir: ['~/.agents'],
|
|
115
|
+
agent: 'cursor',
|
|
116
|
+
conflictPolicy: 'ask',
|
|
117
|
+
mapping: {
|
|
118
|
+
cursor: {
|
|
119
|
+
skillsDir: '.agents/skills',
|
|
120
|
+
rulesDir: '.cursor/rules',
|
|
121
|
+
mcpFile: '.cursor/mcp.json',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
说明:
|
|
128
|
+
|
|
129
|
+
- 配置 key:`sourceDir`、`agent`
|
|
130
|
+
- `sourceDir` 与 `agent` 支持字符串或字符串数组(数组取第一个值)
|
|
131
|
+
|
|
132
|
+
### 6.2 参数优先级
|
|
133
|
+
|
|
134
|
+
1. CLI 参数
|
|
135
|
+
2. 配置文件
|
|
136
|
+
3. 内置默认值
|
|
137
|
+
|
|
138
|
+
## 7. 同步策略说明
|
|
139
|
+
|
|
140
|
+
### 7.1 skills
|
|
141
|
+
|
|
142
|
+
- 发现源目录中的技能目录(需包含 `SKILL.md`)
|
|
143
|
+
- 文件级复制
|
|
144
|
+
- 冲突时:
|
|
145
|
+
- `--yes`:覆盖
|
|
146
|
+
- 无 `--yes`:跳过冲突文件,但继续同步同 skill 下其他文件
|
|
147
|
+
|
|
148
|
+
### 7.2 rules(Cursor)
|
|
149
|
+
|
|
150
|
+
- 发现 `.md`/`.mdc` 规则文件
|
|
151
|
+
- 落盘到 `.cursor/rules/`
|
|
152
|
+
- `.md` 自动转 `.mdc` 后缀
|
|
153
|
+
|
|
154
|
+
### 7.3 mcp(Cursor)
|
|
155
|
+
|
|
156
|
+
- 读取源 `mcp.json`(或 `.cursor/mcp.json`)
|
|
157
|
+
- 标准化后渲染为 Cursor 可用 JSON
|
|
158
|
+
- 写入 `.cursor/mcp.json`
|
|
159
|
+
|
|
160
|
+
## 8. 路径映射与导入约定
|
|
161
|
+
|
|
162
|
+
- TS 路径别名:`@/* -> src/*`
|
|
163
|
+
- 源码采用 `@/` 导入
|
|
164
|
+
- `test/` 目录保持相对路径导入(便于测试语义清晰)
|
|
165
|
+
|
|
166
|
+
## 9. 测试与质量保障
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
pnpm test
|
|
170
|
+
pnpm typecheck
|
|
171
|
+
pnpm lint
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
当前已覆盖:
|
|
175
|
+
|
|
176
|
+
- 配置解析优先级与默认值
|
|
177
|
+
- home 路径展开
|
|
178
|
+
- skills/rules/mcp 核心同步路径
|
|
179
|
+
- mcp 渲染目标限制
|
|
180
|
+
|
|
181
|
+
## 10. 已知限制与后续计划
|
|
182
|
+
|
|
183
|
+
- v1 的 `rule/mcp` 仅支持 Cursor
|
|
184
|
+
- 其他 Agent 的 rules/mcp 路径仅做档案保留,后续扩展
|
|
185
|
+
- 后续可增强:
|
|
186
|
+
- `--diff` 冲突前预览
|
|
187
|
+
- 更细粒度覆盖策略
|
|
188
|
+
- 更多资源类型(prompts/templates 等)
|
|
189
|
+
|
|
190
|
+
## 11. 目录结构(当前实现)
|
|
191
|
+
|
|
192
|
+
```text
|
|
193
|
+
src/
|
|
194
|
+
cli/
|
|
195
|
+
commands/
|
|
196
|
+
skill.ts
|
|
197
|
+
rule.ts
|
|
198
|
+
mcp.ts
|
|
199
|
+
common.ts
|
|
200
|
+
core/
|
|
201
|
+
agent-profiles.ts
|
|
202
|
+
config-loader.ts
|
|
203
|
+
sync-engine.ts
|
|
204
|
+
path.ts
|
|
205
|
+
types/
|
|
206
|
+
core.ts
|
|
207
|
+
config.ts
|
|
208
|
+
index.ts
|
|
209
|
+
test/
|
|
210
|
+
*.test.ts
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## 12. 对外发布前建议
|
|
214
|
+
|
|
215
|
+
- 补充英文版 README
|
|
216
|
+
- 增加 CLI 帮助输出截图/示例
|
|
217
|
+
- 明确 changelog 中的“支持矩阵”
|
|
218
|
+
- 补一组命令级集成测试(尤其错误与跳过提示)
|
package/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# @tofrankie/agents-sync
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@tofrankie/agents-sync) [](https://nodejs.org) [](https://github.com/tofrankie/agents-sync/blob/main/LICENSE) [](https://www.npmjs.com/package/@tofrankie/agents-sync)
|
|
4
|
+
|
|
5
|
+
🤯 WIP...
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process$1 from "node:process";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import c from "ansis";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { loadConfig } from "c12";
|
|
9
|
+
import jiti from "jiti";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
//#region src/core/agent-profiles.ts
|
|
12
|
+
const AGENT_PROFILES = {
|
|
13
|
+
cursor: {
|
|
14
|
+
id: "cursor",
|
|
15
|
+
supports: {
|
|
16
|
+
skill: true,
|
|
17
|
+
rule: true,
|
|
18
|
+
mcp: true
|
|
19
|
+
},
|
|
20
|
+
skillsProjectDirs: [".agents/skills", ".cursor/skills"],
|
|
21
|
+
skillsUserDirs: ["~/.agents/skills", "~/.cursor/skills"],
|
|
22
|
+
rulesProjectDir: ".cursor/rules",
|
|
23
|
+
mcpProjectFile: ".cursor/mcp.json"
|
|
24
|
+
},
|
|
25
|
+
claude: {
|
|
26
|
+
id: "claude",
|
|
27
|
+
supports: {
|
|
28
|
+
skill: true,
|
|
29
|
+
rule: false,
|
|
30
|
+
mcp: false
|
|
31
|
+
},
|
|
32
|
+
skillsProjectDirs: [".claude/skills", ".agents/skills"],
|
|
33
|
+
skillsUserDirs: ["~/.claude/skills", "~/.agents/skills"],
|
|
34
|
+
rulesProjectDir: ".claude/rules",
|
|
35
|
+
mcpProjectFile: ".mcp.json"
|
|
36
|
+
},
|
|
37
|
+
codex: {
|
|
38
|
+
id: "codex",
|
|
39
|
+
supports: {
|
|
40
|
+
skill: true,
|
|
41
|
+
rule: false,
|
|
42
|
+
mcp: false
|
|
43
|
+
},
|
|
44
|
+
skillsProjectDirs: [".agents/skills", ".codex/skills"],
|
|
45
|
+
skillsUserDirs: ["~/.agents/skills", "~/.codex/skills"],
|
|
46
|
+
rulesProjectDir: ".codex/rules",
|
|
47
|
+
mcpProjectFile: ".codex/config.toml"
|
|
48
|
+
},
|
|
49
|
+
gemini: {
|
|
50
|
+
id: "gemini",
|
|
51
|
+
supports: {
|
|
52
|
+
skill: true,
|
|
53
|
+
rule: false,
|
|
54
|
+
mcp: false
|
|
55
|
+
},
|
|
56
|
+
skillsProjectDirs: [".agents/skills"],
|
|
57
|
+
skillsUserDirs: ["~/.agents/skills"]
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
function isValidAgent(id) {
|
|
61
|
+
return id in AGENT_PROFILES;
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/cli/common.ts
|
|
65
|
+
function resolveSkillsTargetDir(cwd) {
|
|
66
|
+
return path.join(cwd, ".agents/skills");
|
|
67
|
+
}
|
|
68
|
+
function resolveRulesTargetDir(cwd) {
|
|
69
|
+
return path.join(cwd, AGENT_PROFILES.cursor.rulesProjectDir ?? ".cursor/rules");
|
|
70
|
+
}
|
|
71
|
+
function resolveMcpTargetFile(cwd) {
|
|
72
|
+
return path.join(cwd, AGENT_PROFILES.cursor.mcpProjectFile ?? ".cursor/mcp.json");
|
|
73
|
+
}
|
|
74
|
+
function printSummary(kind, summary) {
|
|
75
|
+
const kindLabel = kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
76
|
+
p.log.info("Done");
|
|
77
|
+
p.outro(`${kindLabel}: added=${c.green(String(summary.added))}, overwritten=${c.green(String(summary.overwritten))}, skipped=${c.green(String(summary.skipped))}`);
|
|
78
|
+
}
|
|
79
|
+
function printSkip(message) {
|
|
80
|
+
p.log.info("Skip");
|
|
81
|
+
p.outro(message);
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/core/path.ts
|
|
85
|
+
function expandHome(input) {
|
|
86
|
+
if (input === "~") return os.homedir();
|
|
87
|
+
if (input.startsWith("~/")) return path.join(os.homedir(), input.slice(2));
|
|
88
|
+
return input;
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/core/config-loader.ts
|
|
92
|
+
const DEFAULTS = {
|
|
93
|
+
source: "~/.agents",
|
|
94
|
+
targetAgent: "cursor"
|
|
95
|
+
};
|
|
96
|
+
function pickFirst(value) {
|
|
97
|
+
if (Array.isArray(value)) return value[0];
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
async function loadUserConfig(cwd, configPath) {
|
|
101
|
+
if (configPath) {
|
|
102
|
+
const loaded = await jiti(cwd, { interopDefault: true }).import(path.resolve(cwd, configPath));
|
|
103
|
+
if (loaded && typeof loaded === "object" && "default" in loaded) return loaded.default ?? {};
|
|
104
|
+
return loaded ?? {};
|
|
105
|
+
}
|
|
106
|
+
return (await loadConfig({
|
|
107
|
+
cwd,
|
|
108
|
+
name: "agents-sync",
|
|
109
|
+
defaults: {},
|
|
110
|
+
jitiOptions: { interopDefault: true }
|
|
111
|
+
})).config ?? {};
|
|
112
|
+
}
|
|
113
|
+
function resolveOptions(config, cli) {
|
|
114
|
+
const sourceFromConfig = pickFirst(config.sourceDir);
|
|
115
|
+
const targetFromConfig = pickFirst(config.agent);
|
|
116
|
+
const source = cli.source ?? sourceFromConfig ?? DEFAULTS.source;
|
|
117
|
+
const targetAgent = cli.targetAgent ?? targetFromConfig ?? DEFAULTS.targetAgent;
|
|
118
|
+
return {
|
|
119
|
+
source: path.resolve(expandHome(source)),
|
|
120
|
+
targetAgent,
|
|
121
|
+
dryRun: Boolean(cli.dryRun),
|
|
122
|
+
yes: Boolean(cli.yes)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/core/sync-engine.ts
|
|
127
|
+
async function exists(filePath) {
|
|
128
|
+
try {
|
|
129
|
+
await fs.access(filePath);
|
|
130
|
+
return true;
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function listFilesRecursively(root) {
|
|
136
|
+
const out = [];
|
|
137
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
const abs = path.join(root, entry.name);
|
|
140
|
+
if (entry.isDirectory()) out.push(...await listFilesRecursively(abs));
|
|
141
|
+
else out.push(abs);
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
async function discoverSkills(source) {
|
|
146
|
+
const candidates = [path.join(source, "skills"), source];
|
|
147
|
+
const found = [];
|
|
148
|
+
for (const candidate of candidates) {
|
|
149
|
+
if (!await exists(candidate)) continue;
|
|
150
|
+
const entries = await fs.readdir(candidate, { withFileTypes: true });
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (!entry.isDirectory()) continue;
|
|
153
|
+
const skillDir = path.join(candidate, entry.name);
|
|
154
|
+
if (await exists(path.join(skillDir, "SKILL.md"))) found.push(skillDir);
|
|
155
|
+
}
|
|
156
|
+
if (found.length > 0) break;
|
|
157
|
+
}
|
|
158
|
+
return found;
|
|
159
|
+
}
|
|
160
|
+
async function discoverRuleFiles(source) {
|
|
161
|
+
const candidates = [
|
|
162
|
+
path.join(source, "rules"),
|
|
163
|
+
path.join(source, ".cursor/rules"),
|
|
164
|
+
source
|
|
165
|
+
];
|
|
166
|
+
for (const candidate of candidates) {
|
|
167
|
+
if (!await exists(candidate)) continue;
|
|
168
|
+
const rules = (await fs.readdir(candidate, { withFileTypes: true })).filter((entry) => entry.isFile() && (entry.name.endsWith(".md") || entry.name.endsWith(".mdc"))).map((entry) => path.join(candidate, entry.name));
|
|
169
|
+
if (rules.length > 0) return rules;
|
|
170
|
+
}
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
async function resolveMcpSourceFile(source) {
|
|
174
|
+
const candidates = [path.join(source, "mcp.json"), path.join(source, ".cursor/mcp.json")];
|
|
175
|
+
for (const candidate of candidates) if (await exists(candidate)) return candidate;
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
async function copySkillDir(sourceDir, targetDir, ctx) {
|
|
179
|
+
const files = await listFilesRecursively(sourceDir);
|
|
180
|
+
let added = 0;
|
|
181
|
+
let overwritten = 0;
|
|
182
|
+
let skipped = 0;
|
|
183
|
+
for (const file of files) {
|
|
184
|
+
const rel = path.relative(sourceDir, file);
|
|
185
|
+
const target = path.join(targetDir, rel);
|
|
186
|
+
const hasTarget = await exists(target);
|
|
187
|
+
if (hasTarget && !ctx.yes) {
|
|
188
|
+
skipped += 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (!ctx.dryRun) {
|
|
192
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
193
|
+
await fs.copyFile(file, target);
|
|
194
|
+
}
|
|
195
|
+
if (hasTarget) overwritten += 1;
|
|
196
|
+
else added += 1;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
added,
|
|
200
|
+
overwritten,
|
|
201
|
+
skipped
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async function copyRuleFiles(sourceFiles, targetDir, ctx) {
|
|
205
|
+
let added = 0;
|
|
206
|
+
let overwritten = 0;
|
|
207
|
+
let skipped = 0;
|
|
208
|
+
if (!ctx.dryRun && sourceFiles.length > 0) await fs.mkdir(targetDir, { recursive: true });
|
|
209
|
+
for (const file of sourceFiles) {
|
|
210
|
+
const name = path.basename(file).replace(/\.md$/, ".mdc");
|
|
211
|
+
const target = path.join(targetDir, name);
|
|
212
|
+
const hasTarget = await exists(target);
|
|
213
|
+
if (hasTarget && !ctx.yes) {
|
|
214
|
+
skipped += 1;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (!ctx.dryRun) await fs.copyFile(file, target);
|
|
218
|
+
if (hasTarget) overwritten += 1;
|
|
219
|
+
else added += 1;
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
added,
|
|
223
|
+
overwritten,
|
|
224
|
+
skipped
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async function normalizeMcpConfig(sourceFile) {
|
|
228
|
+
const raw = await fs.readFile(sourceFile, "utf8");
|
|
229
|
+
return JSON.parse(raw);
|
|
230
|
+
}
|
|
231
|
+
function renderMcpConfigByAgent(model, targetAgent) {
|
|
232
|
+
if (targetAgent !== "cursor") throw new Error(`unsupported mcp target: ${targetAgent}`);
|
|
233
|
+
return `${JSON.stringify(model, null, 2)}\n`;
|
|
234
|
+
}
|
|
235
|
+
//#endregion
|
|
236
|
+
//#region src/cli/commands/mcp.ts
|
|
237
|
+
async function runMcpCommand(cwd, options) {
|
|
238
|
+
const runtime = resolveOptions(await loadUserConfig(cwd, options.config), options);
|
|
239
|
+
if (runtime.targetAgent !== "cursor") {
|
|
240
|
+
printSkip(`MCP sync is currently supported only for Cursor. Use --target-agent cursor (current: ${runtime.targetAgent}).`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const sourceFile = await resolveMcpSourceFile(runtime.source);
|
|
244
|
+
if (!sourceFile) {
|
|
245
|
+
printSkip(`No MCP config file found in ${runtime.source}.`);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const rendered = renderMcpConfigByAgent(await normalizeMcpConfig(sourceFile), runtime.targetAgent);
|
|
249
|
+
const targetFile = resolveMcpTargetFile(cwd);
|
|
250
|
+
if (runtime.dryRun) {
|
|
251
|
+
process.stdout.write(`dry-run: MCP sync target => ${targetFile}\n`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
await fs.mkdir(path.dirname(targetFile), { recursive: true });
|
|
255
|
+
await fs.writeFile(targetFile, rendered, "utf8");
|
|
256
|
+
process.stdout.write(`done: MCP synced to ${targetFile}\n`);
|
|
257
|
+
}
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/cli/commands/rule.ts
|
|
260
|
+
async function runRuleCommand(cwd, options) {
|
|
261
|
+
const runtime = resolveOptions(await loadUserConfig(cwd, options.config), options);
|
|
262
|
+
if (runtime.targetAgent !== "cursor") {
|
|
263
|
+
printSkip(`Rules sync is currently supported only for Cursor. Use --target-agent cursor (current: ${runtime.targetAgent}).`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const files = await discoverRuleFiles(runtime.source);
|
|
267
|
+
if (files.length === 0) {
|
|
268
|
+
printSkip(`No rule files found in ${runtime.source}.`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
printSummary("rules", await copyRuleFiles(files, resolveRulesTargetDir(cwd), {
|
|
272
|
+
dryRun: runtime.dryRun,
|
|
273
|
+
yes: runtime.yes
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/cli/commands/skill.ts
|
|
278
|
+
async function runSkillCommand(cwd, options) {
|
|
279
|
+
const runtime = resolveOptions(await loadUserConfig(cwd, options.config), options);
|
|
280
|
+
const skillDirs = await discoverSkills(runtime.source);
|
|
281
|
+
if (skillDirs.length === 0) {
|
|
282
|
+
printSkip(`No syncable skills found in ${runtime.source}.`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
let selected = skillDirs;
|
|
286
|
+
if (!runtime.yes) {
|
|
287
|
+
const picked = await p.multiselect({
|
|
288
|
+
message: `Select ${c.green("skills")} to sync`,
|
|
289
|
+
options: skillDirs.map((dir) => ({
|
|
290
|
+
label: path.basename(dir),
|
|
291
|
+
value: dir
|
|
292
|
+
})),
|
|
293
|
+
required: false
|
|
294
|
+
});
|
|
295
|
+
if (p.isCancel(picked)) {
|
|
296
|
+
p.cancel("Operation canceled.");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
selected = picked;
|
|
300
|
+
}
|
|
301
|
+
if (selected.length === 0) {
|
|
302
|
+
printSkip("No skills selected.");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (!isValidAgent(runtime.targetAgent)) {
|
|
306
|
+
printSkip(`Unknown target agent: ${runtime.targetAgent}`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
let total = {
|
|
310
|
+
added: 0,
|
|
311
|
+
overwritten: 0,
|
|
312
|
+
skipped: 0
|
|
313
|
+
};
|
|
314
|
+
const targetRoot = resolveSkillsTargetDir(cwd);
|
|
315
|
+
for (const dir of selected) {
|
|
316
|
+
const summary = await copySkillDir(dir, path.join(targetRoot, path.basename(dir)), {
|
|
317
|
+
dryRun: runtime.dryRun,
|
|
318
|
+
yes: runtime.yes
|
|
319
|
+
});
|
|
320
|
+
total = {
|
|
321
|
+
added: total.added + summary.added,
|
|
322
|
+
overwritten: total.overwritten + summary.overwritten,
|
|
323
|
+
skipped: total.skipped + summary.skipped
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
printSummary("skills", total);
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region package.json
|
|
330
|
+
var name = "@tofrankie/agents-sync";
|
|
331
|
+
var version = "0.0.1";
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/cli/index.ts
|
|
334
|
+
main().catch((error) => {
|
|
335
|
+
process$1.stdout.write("\n");
|
|
336
|
+
process$1.stderr.write(`${String(error)}\n`);
|
|
337
|
+
process$1.stdout.write("\n");
|
|
338
|
+
process$1.exitCode = 1;
|
|
339
|
+
});
|
|
340
|
+
async function main() {
|
|
341
|
+
const program = new Command();
|
|
342
|
+
program.name("agents-sync").description("Sync skills, cursor rules, and cursor mcp config from a source folder.").version(version, "-v, --version", "output the version number").hook("preAction", () => {
|
|
343
|
+
process$1.stdout.write("\n");
|
|
344
|
+
p.intro(`${name} ${c.dim(`v${version}`)}`);
|
|
345
|
+
});
|
|
346
|
+
applySharedOptions(program.command("skill").description("sync skills")).action(async (options) => {
|
|
347
|
+
await runSkillCommand(process$1.cwd(), options);
|
|
348
|
+
});
|
|
349
|
+
applySharedOptions(program.command("rule").description("sync rules (cursor only)")).action(async (options) => {
|
|
350
|
+
await runRuleCommand(process$1.cwd(), options);
|
|
351
|
+
});
|
|
352
|
+
applySharedOptions(program.command("mcp").description("sync mcp config (cursor only)")).action(async (options) => {
|
|
353
|
+
await runMcpCommand(process$1.cwd(), options);
|
|
354
|
+
});
|
|
355
|
+
await program.parseAsync(process$1.argv);
|
|
356
|
+
}
|
|
357
|
+
function applySharedOptions(cmd) {
|
|
358
|
+
return cmd.option("-s, --source <path>", "source path, default ~/.agents").option("-t, --target-agent <agent>", "target agent, default cursor").option("-c, --config <path>", "config file path").option("--dry-run", "show planned operations without writing files").option("-y, --yes", "skip prompts and overwrite conflicts");
|
|
359
|
+
}
|
|
360
|
+
//#endregion
|
|
361
|
+
export {};
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/types/core.d.ts
|
|
2
|
+
type AgentId = 'cursor' | 'claude' | 'codex' | 'gemini';
|
|
3
|
+
//#endregion
|
|
4
|
+
//#region src/types/config.d.ts
|
|
5
|
+
interface AgentsSyncConfig {
|
|
6
|
+
/** Source directory path(s). Default: `~/.agents`. If an array is provided, only the first item is used. */
|
|
7
|
+
sourceDir?: string | string[];
|
|
8
|
+
/** Target agent(s). Default: `cursor`. If an array is provided, only the first item is used. */
|
|
9
|
+
agent?: AgentId | AgentId[];
|
|
10
|
+
/** Default conflict behavior when target files already exist. */
|
|
11
|
+
conflictPolicy?: 'ask' | 'overwrite' | 'skip';
|
|
12
|
+
/** Per-agent path overrides for skills, rules, and mcp outputs. */
|
|
13
|
+
mapping?: Partial<Record<AgentId, {
|
|
14
|
+
skillsDir?: string;
|
|
15
|
+
rulesDir?: string;
|
|
16
|
+
mcpFile?: string;
|
|
17
|
+
}>>;
|
|
18
|
+
}
|
|
19
|
+
declare function defineConfig(config: AgentsSyncConfig): AgentsSyncConfig;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { type AgentsSyncConfig, defineConfig };
|
package/dist/index.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tofrankie/agents-sync",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Sync skills, rules, and MCP from a configurable source into your project",
|
|
6
|
+
"author": "Frankie <1426203851@qq.com> (https://github.com/tofrankie)",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/tofrankie/agents-sync#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/tofrankie/agents-sync.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/tofrankie/agents-sync/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"agents",
|
|
18
|
+
"sync",
|
|
19
|
+
"cursor",
|
|
20
|
+
"claude",
|
|
21
|
+
"codex",
|
|
22
|
+
"gemini",
|
|
23
|
+
"skills",
|
|
24
|
+
"mcp",
|
|
25
|
+
"rules"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.mts",
|
|
33
|
+
"import": "./dist/index.mjs"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"main": "./dist/index.mjs",
|
|
37
|
+
"types": "./dist/index.d.mts",
|
|
38
|
+
"bin": {
|
|
39
|
+
"agents-sync": "./dist/cli.mjs"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"CHANGELOG.md",
|
|
43
|
+
"dist"
|
|
44
|
+
],
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=20.19.0"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@clack/prompts": "^1.2.0",
|
|
50
|
+
"ansis": "^4.2.0",
|
|
51
|
+
"c12": "^3.3.4",
|
|
52
|
+
"commander": "^14.0.3",
|
|
53
|
+
"jiti": "^2.6.1"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@commitlint/cli": "^20.5.0",
|
|
57
|
+
"@tofrankie/action": "^0.0.7",
|
|
58
|
+
"@tofrankie/commitlint": "^0.0.6",
|
|
59
|
+
"@tofrankie/eslint": "^0.2.1",
|
|
60
|
+
"@tofrankie/prettier": "^0.2.0",
|
|
61
|
+
"@tofrankie/tsconfig": "^0.0.5",
|
|
62
|
+
"@types/node": "^25.6.0",
|
|
63
|
+
"eslint": "^10.2.1",
|
|
64
|
+
"lint-staged": "^16.4.0",
|
|
65
|
+
"npm-run-all2": "^8.0.4",
|
|
66
|
+
"prettier": "^3.8.3",
|
|
67
|
+
"publint": "^0.3.18",
|
|
68
|
+
"simple-git-hooks": "^2.13.1",
|
|
69
|
+
"tsdown": "^0.21.9",
|
|
70
|
+
"typescript": "^5.9.3",
|
|
71
|
+
"vitest": "^4.1.5"
|
|
72
|
+
},
|
|
73
|
+
"simple-git-hooks": {
|
|
74
|
+
"pre-commit": "pnpm lint-staged",
|
|
75
|
+
"commit-msg": "pnpm exec commitlint --edit $1"
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "tsdown",
|
|
79
|
+
"dev": "tsdown --watch",
|
|
80
|
+
"lint": "run-s lint:eslint lint:prettier typecheck",
|
|
81
|
+
"lint:eslint": "eslint . --cache --fix",
|
|
82
|
+
"lint:prettier": "prettier . --cache --write --log-level silent",
|
|
83
|
+
"typecheck": "tsc --noEmit",
|
|
84
|
+
"test": "vitest run",
|
|
85
|
+
"publint": "publint",
|
|
86
|
+
"github:release": "tfr"
|
|
87
|
+
}
|
|
88
|
+
}
|