aicsync 0.1.0__tar.gz
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.
- aicsync-0.1.0/.gitignore +6 -0
- aicsync-0.1.0/LICENSE +21 -0
- aicsync-0.1.0/PKG-INFO +195 -0
- aicsync-0.1.0/README.md +177 -0
- aicsync-0.1.0/aics/__init__.py +1 -0
- aicsync-0.1.0/aics/__main__.py +4 -0
- aicsync-0.1.0/aics/bundle.py +180 -0
- aicsync-0.1.0/aics/cli.py +186 -0
- aicsync-0.1.0/aics/config.py +19 -0
- aicsync-0.1.0/aics/convert.py +33 -0
- aicsync-0.1.0/aics/errors.py +10 -0
- aicsync-0.1.0/aics/installer.py +178 -0
- aicsync-0.1.0/aics/log.py +24 -0
- aicsync-0.1.0/aics/render.py +127 -0
- aicsync-0.1.0/aics/sanitize.py +25 -0
- aicsync-0.1.0/aics/scan.py +74 -0
- aicsync-0.1.0/aics/tui.py +78 -0
- aicsync-0.1.0/aics.py +9 -0
- aicsync-0.1.0/bin/aics +3 -0
- aicsync-0.1.0/pyproject.toml +30 -0
aicsync-0.1.0/.gitignore
ADDED
aicsync-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hanjinxin
|
|
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.
|
aicsync-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: aicsync
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI client config sync CLI — back up / migrate Claude Code & Cursor config (MCP, skills, plugins, rules, extensions).
|
|
5
|
+
Project-URL: Homepage, https://github.com/hanjinxin/aics
|
|
6
|
+
Author: hanjinxin
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: backup,claude-code,cli,config,cursor,mcp,sync
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Utilities
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# aics
|
|
20
|
+
|
|
21
|
+
**AI client config sync** — 在不同机器之间备份/迁移你的 Claude Code 与 Cursor 配置(MCP、skills、plugins、commands、agents、rules、extensions)。
|
|
22
|
+
|
|
23
|
+
纯 Python 标准库,零依赖,零构建。`python3 aics.py` 直接跑。
|
|
24
|
+
|
|
25
|
+
## 为什么需要
|
|
26
|
+
|
|
27
|
+
你在 Claude Code 和 Cursor 上攒了一套个人配置——MCP servers、skills、plugins、自定义命令、Cursor rules、扩展。换机器或重装系统时,这些散落在 `~/.claude.json`、`~/.claude/`、`~/.cursor/` 各处的配置没法一键带走,手动拷贝既容易漏,又容易把明文密钥(API token)带走泄露到别处。
|
|
28
|
+
|
|
29
|
+
市面已有的工具(Smithery、社区 mcp-sync)只管「从零安装 MCP server」,没有「把我这套配置整体打包、到新机器一键还原」的同端迁移工具。aics 填这个空。
|
|
30
|
+
|
|
31
|
+
## 安装
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
git clone <your-repo> aics && cd aics
|
|
35
|
+
chmod +x aics.py bin/aics
|
|
36
|
+
# 可选:加到 PATH
|
|
37
|
+
ln -s "$(pwd)/bin/aics" /usr/local/bin/aics
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
要求:Python 3.10+(仅标准库)。
|
|
41
|
+
|
|
42
|
+
## 全局选项
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
-V, --version 版本号
|
|
46
|
+
-v, --verbose 详细日志输出到 stderr
|
|
47
|
+
-q, --quiet 静默(抑制进度日志)
|
|
48
|
+
-h, --help 帮助
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
结果走 stdout,日志/错误走 stderr,可安全用于管道。退出码:0 成功,1 失败,130 中断。可用 `AICS_HOME` 环境变量覆盖目标 home(测试/沙箱用)。
|
|
52
|
+
|
|
53
|
+
## 交互模式
|
|
54
|
+
|
|
55
|
+
aics 是 **CLI + interactive layer**(非全屏 TUI)。TTY 里自动开,管道/agent 走纯文本:
|
|
56
|
+
|
|
57
|
+
- **无参 `aics`**:TTY 进数字菜单(status/export/list/diff/install/convert/quit),逐项收参;非 TTY 打印 help。
|
|
58
|
+
- **颜色**:TTY 时 status/diff/list 自动加色(标题青、`==` 绿、`+bundle` 黄、redacted 红)。设 `NO_COLOR` 或非 TTY → 纯文本。
|
|
59
|
+
- **install 确认门**:TTY 且非 `--yes` 时,先打彩色 diff,再 `Apply these changes? [y/N]`。答 n 中止(不备份不改);答 y 才备份+应用。非 TTY / `--yes` 跳过确认,自动化路径不变。
|
|
60
|
+
|
|
61
|
+
人类有颜色有确认有菜单,agent / `aics install bundle | bash` / CI 走的仍是干净 stdout + 退出码。
|
|
62
|
+
|
|
63
|
+
## 命令
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
### `status` — 看本机有什么
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python3 aics.py status # 两端都看
|
|
71
|
+
python3 aics.py status --client claude
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
输出 markdown 清单:每个 MCP server 的安装命令、每个 plugin 的 `claude plugin install` 命令、skills/commands/agents 列表、Cursor 扩展列表。密钥默认脱敏。
|
|
75
|
+
|
|
76
|
+
### `export` — 打包配置
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python3 aics.py export -o ./my-bundle # 默认脱敏
|
|
80
|
+
python3 aics.py export -o ./my-bundle --tar # 额外打 tar.gz
|
|
81
|
+
python3 aics.py export -o ./my-bundle --include-secrets # 保留明文密钥
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
生成 bundle 目录:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
my-bundle/
|
|
88
|
+
├── INSTALL.md # agent 可读的安装指南(含每项的安装命令)
|
|
89
|
+
├── manifest.json # 机器可读索引(counts / redacted / assets)
|
|
90
|
+
├── claude_mcp.json # MCP servers + settings + plugins 清单
|
|
91
|
+
├── cursor_config.json # MCP + settings + extensions 清单
|
|
92
|
+
└── assets/
|
|
93
|
+
├── claude/{skills,commands,agents}/ # 纯文本资产,直接拷贝
|
|
94
|
+
└── cursor/rules/ # .mdc 规则文件
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
### `list` — 看 bundle 内容
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
python3 aics.py list ./my-bundle
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
### `diff` — bundle vs 本机
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
python3 aics.py diff ./my-bundle
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
逐项对比 MCP/plugins/skills/extensions 的数量差异。
|
|
114
|
+
|
|
115
|
+
### `install` — 应用到本机
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
python3 aics.py install ./my-bundle # 文件类直接应用,网络类只打印
|
|
119
|
+
python3 aics.py install ./my-bundle --client claude
|
|
120
|
+
python3 aics.py install ./my-bundle --force # 覆盖已存在项
|
|
121
|
+
python3 aics.py install ./my-bundle --yes # 连网络安装(plugins/扩展)一起跑
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
行为:
|
|
125
|
+
|
|
126
|
+
- **执行前**先备份当前配置到 `~/.aics/backup/<timestamp>/`
|
|
127
|
+
- **文件类**(MCP JSON 合并、skills/commands/agents/rules 拷贝、settings 合并)直接执行,幂等(已存在跳过,`--force` 才覆盖)
|
|
128
|
+
- **网络类**(`claude plugin install`、`cursor --install-extension`)默认只打印命令,`--yes` 才真跑
|
|
129
|
+
- **密钥**:脱敏字段保持空值并提示手动回填,绝不猜值
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
### `convert` — Claude skill → Cursor rule
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
python3 aics.py convert --skill ~/.claude/skills/foo/SKILL.md --out foo.mdc
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
把 SKILL.md 的 frontmatter 翻译成 Cursor `.mdc` 格式,无法表达的触发语义打 `<!-- TODO -->`。
|
|
140
|
+
|
|
141
|
+
## 两条使用路径
|
|
142
|
+
|
|
143
|
+
```mermaid
|
|
144
|
+
flowchart LR
|
|
145
|
+
A["aics export"] --> B["bundle + INSTALL.md"]
|
|
146
|
+
B --> C{谁来装?}
|
|
147
|
+
C -->|让 CLI 自己装| D["aics install bundle --yes"]
|
|
148
|
+
C -->|交给 agent| E["agent 读 INSTALL.md\n逐段执行命令"]
|
|
149
|
+
B -.->|密钥| F["redacted 列表\n手动回填"]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Agent 用法
|
|
153
|
+
|
|
154
|
+
非 TTY 自动无色、无确认门、无菜单。stdout 只放结果(可 grep/parse),stderr 放进度日志。exit code:0 成功 / 1 失败 / 130 中断。
|
|
155
|
+
|
|
156
|
+
最小三步:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
aics export -o b # 打包(密钥默认脱敏,产物 b/INSTALL.md + manifest.json)
|
|
160
|
+
aics install b --yes # 应用(非交互,--yes 连网络安装一起跑,幂等可重试)
|
|
161
|
+
aics diff b # 验证(全 == 即还原完成)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
要点:
|
|
165
|
+
- `aics status` stdout 是 markdown,`claude mcp add ...` / `claude plugin install ...` 行可直接执行。
|
|
166
|
+
- 密钥在 bundle 里是 null,**agent 不猜值**;把 `manifest.json` 的 `redacted` 列表抛给用户回填。
|
|
167
|
+
- 沙箱试跑:`AICS_HOME=/tmp/sb aics install b --yes`,不碰真实 home。
|
|
168
|
+
- `install` 幂等,重复执行跳过已存在项。
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
## 设计:插件为什么是「重装」而不是「拷贝」
|
|
175
|
+
|
|
176
|
+
`~/.claude/plugins/cache/` 里的插件缓存含平台编译产物(darwin-x64 二进制)、git 工作树状态、绝对路径,且全部信息可由 `installed_plugins.json` 的 `name@marketplace` + git sha 重建。所以 aics 只导出安装清单,`install` 时跑 `claude plugin install <name>@<marketplace>` 按 sha 精确重装——迁的是「安装意图」,不是「缓存尸体」。
|
|
177
|
+
|
|
178
|
+
skills/commands/agents/rules 是纯文本,直接拷贝,无需重装。
|
|
179
|
+
|
|
180
|
+
## 安全模型
|
|
181
|
+
|
|
182
|
+
- **默认脱敏**:所有 JSON 值中匹配 `token|key|secret|password|auth|credential` 的字段在 export/status 时置空,记入 `manifest.redacted`
|
|
183
|
+
- `--include-secrets` 显式 opt-in 才保留明文
|
|
184
|
+
- `install` 时不猜密钥值,提示用户手动回填
|
|
185
|
+
- `install` 前自动备份原配置
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
## 客户端适配
|
|
190
|
+
|
|
191
|
+
目前支持 Claude Code + Cursor。新增客户端只需写一个 scan 函数 + apply 函数。
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT
|
aicsync-0.1.0/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# aics
|
|
2
|
+
|
|
3
|
+
**AI client config sync** — 在不同机器之间备份/迁移你的 Claude Code 与 Cursor 配置(MCP、skills、plugins、commands、agents、rules、extensions)。
|
|
4
|
+
|
|
5
|
+
纯 Python 标准库,零依赖,零构建。`python3 aics.py` 直接跑。
|
|
6
|
+
|
|
7
|
+
## 为什么需要
|
|
8
|
+
|
|
9
|
+
你在 Claude Code 和 Cursor 上攒了一套个人配置——MCP servers、skills、plugins、自定义命令、Cursor rules、扩展。换机器或重装系统时,这些散落在 `~/.claude.json`、`~/.claude/`、`~/.cursor/` 各处的配置没法一键带走,手动拷贝既容易漏,又容易把明文密钥(API token)带走泄露到别处。
|
|
10
|
+
|
|
11
|
+
市面已有的工具(Smithery、社区 mcp-sync)只管「从零安装 MCP server」,没有「把我这套配置整体打包、到新机器一键还原」的同端迁移工具。aics 填这个空。
|
|
12
|
+
|
|
13
|
+
## 安装
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git clone <your-repo> aics && cd aics
|
|
17
|
+
chmod +x aics.py bin/aics
|
|
18
|
+
# 可选:加到 PATH
|
|
19
|
+
ln -s "$(pwd)/bin/aics" /usr/local/bin/aics
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
要求:Python 3.10+(仅标准库)。
|
|
23
|
+
|
|
24
|
+
## 全局选项
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
-V, --version 版本号
|
|
28
|
+
-v, --verbose 详细日志输出到 stderr
|
|
29
|
+
-q, --quiet 静默(抑制进度日志)
|
|
30
|
+
-h, --help 帮助
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
结果走 stdout,日志/错误走 stderr,可安全用于管道。退出码:0 成功,1 失败,130 中断。可用 `AICS_HOME` 环境变量覆盖目标 home(测试/沙箱用)。
|
|
34
|
+
|
|
35
|
+
## 交互模式
|
|
36
|
+
|
|
37
|
+
aics 是 **CLI + interactive layer**(非全屏 TUI)。TTY 里自动开,管道/agent 走纯文本:
|
|
38
|
+
|
|
39
|
+
- **无参 `aics`**:TTY 进数字菜单(status/export/list/diff/install/convert/quit),逐项收参;非 TTY 打印 help。
|
|
40
|
+
- **颜色**:TTY 时 status/diff/list 自动加色(标题青、`==` 绿、`+bundle` 黄、redacted 红)。设 `NO_COLOR` 或非 TTY → 纯文本。
|
|
41
|
+
- **install 确认门**:TTY 且非 `--yes` 时,先打彩色 diff,再 `Apply these changes? [y/N]`。答 n 中止(不备份不改);答 y 才备份+应用。非 TTY / `--yes` 跳过确认,自动化路径不变。
|
|
42
|
+
|
|
43
|
+
人类有颜色有确认有菜单,agent / `aics install bundle | bash` / CI 走的仍是干净 stdout + 退出码。
|
|
44
|
+
|
|
45
|
+
## 命令
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
### `status` — 看本机有什么
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python3 aics.py status # 两端都看
|
|
53
|
+
python3 aics.py status --client claude
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
输出 markdown 清单:每个 MCP server 的安装命令、每个 plugin 的 `claude plugin install` 命令、skills/commands/agents 列表、Cursor 扩展列表。密钥默认脱敏。
|
|
57
|
+
|
|
58
|
+
### `export` — 打包配置
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python3 aics.py export -o ./my-bundle # 默认脱敏
|
|
62
|
+
python3 aics.py export -o ./my-bundle --tar # 额外打 tar.gz
|
|
63
|
+
python3 aics.py export -o ./my-bundle --include-secrets # 保留明文密钥
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
生成 bundle 目录:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
my-bundle/
|
|
70
|
+
├── INSTALL.md # agent 可读的安装指南(含每项的安装命令)
|
|
71
|
+
├── manifest.json # 机器可读索引(counts / redacted / assets)
|
|
72
|
+
├── claude_mcp.json # MCP servers + settings + plugins 清单
|
|
73
|
+
├── cursor_config.json # MCP + settings + extensions 清单
|
|
74
|
+
└── assets/
|
|
75
|
+
├── claude/{skills,commands,agents}/ # 纯文本资产,直接拷贝
|
|
76
|
+
└── cursor/rules/ # .mdc 规则文件
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
### `list` — 看 bundle 内容
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
python3 aics.py list ./my-bundle
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
### `diff` — bundle vs 本机
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python3 aics.py diff ./my-bundle
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
逐项对比 MCP/plugins/skills/extensions 的数量差异。
|
|
96
|
+
|
|
97
|
+
### `install` — 应用到本机
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
python3 aics.py install ./my-bundle # 文件类直接应用,网络类只打印
|
|
101
|
+
python3 aics.py install ./my-bundle --client claude
|
|
102
|
+
python3 aics.py install ./my-bundle --force # 覆盖已存在项
|
|
103
|
+
python3 aics.py install ./my-bundle --yes # 连网络安装(plugins/扩展)一起跑
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
行为:
|
|
107
|
+
|
|
108
|
+
- **执行前**先备份当前配置到 `~/.aics/backup/<timestamp>/`
|
|
109
|
+
- **文件类**(MCP JSON 合并、skills/commands/agents/rules 拷贝、settings 合并)直接执行,幂等(已存在跳过,`--force` 才覆盖)
|
|
110
|
+
- **网络类**(`claude plugin install`、`cursor --install-extension`)默认只打印命令,`--yes` 才真跑
|
|
111
|
+
- **密钥**:脱敏字段保持空值并提示手动回填,绝不猜值
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
### `convert` — Claude skill → Cursor rule
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
python3 aics.py convert --skill ~/.claude/skills/foo/SKILL.md --out foo.mdc
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
把 SKILL.md 的 frontmatter 翻译成 Cursor `.mdc` 格式,无法表达的触发语义打 `<!-- TODO -->`。
|
|
122
|
+
|
|
123
|
+
## 两条使用路径
|
|
124
|
+
|
|
125
|
+
```mermaid
|
|
126
|
+
flowchart LR
|
|
127
|
+
A["aics export"] --> B["bundle + INSTALL.md"]
|
|
128
|
+
B --> C{谁来装?}
|
|
129
|
+
C -->|让 CLI 自己装| D["aics install bundle --yes"]
|
|
130
|
+
C -->|交给 agent| E["agent 读 INSTALL.md\n逐段执行命令"]
|
|
131
|
+
B -.->|密钥| F["redacted 列表\n手动回填"]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Agent 用法
|
|
135
|
+
|
|
136
|
+
非 TTY 自动无色、无确认门、无菜单。stdout 只放结果(可 grep/parse),stderr 放进度日志。exit code:0 成功 / 1 失败 / 130 中断。
|
|
137
|
+
|
|
138
|
+
最小三步:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
aics export -o b # 打包(密钥默认脱敏,产物 b/INSTALL.md + manifest.json)
|
|
142
|
+
aics install b --yes # 应用(非交互,--yes 连网络安装一起跑,幂等可重试)
|
|
143
|
+
aics diff b # 验证(全 == 即还原完成)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
要点:
|
|
147
|
+
- `aics status` stdout 是 markdown,`claude mcp add ...` / `claude plugin install ...` 行可直接执行。
|
|
148
|
+
- 密钥在 bundle 里是 null,**agent 不猜值**;把 `manifest.json` 的 `redacted` 列表抛给用户回填。
|
|
149
|
+
- 沙箱试跑:`AICS_HOME=/tmp/sb aics install b --yes`,不碰真实 home。
|
|
150
|
+
- `install` 幂等,重复执行跳过已存在项。
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
## 设计:插件为什么是「重装」而不是「拷贝」
|
|
157
|
+
|
|
158
|
+
`~/.claude/plugins/cache/` 里的插件缓存含平台编译产物(darwin-x64 二进制)、git 工作树状态、绝对路径,且全部信息可由 `installed_plugins.json` 的 `name@marketplace` + git sha 重建。所以 aics 只导出安装清单,`install` 时跑 `claude plugin install <name>@<marketplace>` 按 sha 精确重装——迁的是「安装意图」,不是「缓存尸体」。
|
|
159
|
+
|
|
160
|
+
skills/commands/agents/rules 是纯文本,直接拷贝,无需重装。
|
|
161
|
+
|
|
162
|
+
## 安全模型
|
|
163
|
+
|
|
164
|
+
- **默认脱敏**:所有 JSON 值中匹配 `token|key|secret|password|auth|credential` 的字段在 export/status 时置空,记入 `manifest.redacted`
|
|
165
|
+
- `--include-secrets` 显式 opt-in 才保留明文
|
|
166
|
+
- `install` 时不猜密钥值,提示用户手动回填
|
|
167
|
+
- `install` 前自动备份原配置
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
## 客户端适配
|
|
172
|
+
|
|
173
|
+
目前支持 Claude Code + Cursor。新增客户端只需写一个 scan 函数 + apply 函数。
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import shutil
|
|
4
|
+
import tarfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .config import CLAUDE_DIR, CURSOR_DIR
|
|
8
|
+
from .render import now_iso, host, render_markdown
|
|
9
|
+
from .sanitize import sanitize
|
|
10
|
+
from .scan import scan_claude, scan_cursor
|
|
11
|
+
|
|
12
|
+
_c = {
|
|
13
|
+
"reset": "0",
|
|
14
|
+
"bold": "1",
|
|
15
|
+
"dim": "2",
|
|
16
|
+
"red": "31",
|
|
17
|
+
"green": "32",
|
|
18
|
+
"yellow": "33",
|
|
19
|
+
"blue": "34",
|
|
20
|
+
"magenta": "35",
|
|
21
|
+
"cyan": "36",
|
|
22
|
+
"gray": "90",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def sha256_file(p: Path):
|
|
27
|
+
h = hashlib.sha256()
|
|
28
|
+
with open(p, "rb") as f:
|
|
29
|
+
for chunk in iter(lambda: f.read(65536), b""):
|
|
30
|
+
h.update(chunk)
|
|
31
|
+
return h.hexdigest()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def copy_dir(src: Path, dst: Path):
|
|
35
|
+
if not src.exists():
|
|
36
|
+
return []
|
|
37
|
+
copied = []
|
|
38
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
for item in src.iterdir():
|
|
40
|
+
if item.name.startswith("."):
|
|
41
|
+
continue
|
|
42
|
+
target = dst / item.name
|
|
43
|
+
if item.is_dir():
|
|
44
|
+
shutil.copytree(item, target, dirs_exist_ok=True)
|
|
45
|
+
for f in target.rglob("*"):
|
|
46
|
+
if f.is_file():
|
|
47
|
+
copied.append(str(f.relative_to(dst.parent.parent)))
|
|
48
|
+
else:
|
|
49
|
+
shutil.copy2(item, target)
|
|
50
|
+
copied.append(str(target.relative_to(dst.parent.parent)))
|
|
51
|
+
return copied
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def export_bundle(out_dir: Path, include_secrets=False):
|
|
55
|
+
claude = scan_claude()
|
|
56
|
+
cursor = scan_cursor()
|
|
57
|
+
csettings, cred = sanitize(claude["settings"], include_secrets)
|
|
58
|
+
cursor_settings, rred = sanitize(cursor["settings"], include_secrets)
|
|
59
|
+
claude["settings"] = csettings
|
|
60
|
+
cursor["settings"] = cursor_settings
|
|
61
|
+
redacted = cred + rred
|
|
62
|
+
|
|
63
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
assets = out_dir / "assets"
|
|
65
|
+
asset_files = []
|
|
66
|
+
for kind in ("skills", "commands", "agents"):
|
|
67
|
+
asset_files += copy_dir(CLAUDE_DIR / kind, assets / "claude" / kind)
|
|
68
|
+
asset_files += copy_dir(CURSOR_DIR / "rules", assets / "cursor" / "rules")
|
|
69
|
+
|
|
70
|
+
(out_dir / "claude_mcp.json").write_text(
|
|
71
|
+
json.dumps(
|
|
72
|
+
{
|
|
73
|
+
"global": claude["mcpServers"],
|
|
74
|
+
"projects": claude["projects"],
|
|
75
|
+
"settings": csettings,
|
|
76
|
+
"plugins": claude["plugins"],
|
|
77
|
+
"enabledPlugins": claude["enabledPlugins"],
|
|
78
|
+
},
|
|
79
|
+
indent=2,
|
|
80
|
+
ensure_ascii=False,
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
(out_dir / "cursor_config.json").write_text(
|
|
84
|
+
json.dumps(
|
|
85
|
+
{"mcp": cursor["mcp"], "settings": cursor_settings, "extensions": cursor["extensions"]},
|
|
86
|
+
indent=2,
|
|
87
|
+
ensure_ascii=False,
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
(out_dir / "INSTALL.md").write_text(render_markdown(claude, cursor, redacted, guide=True))
|
|
91
|
+
|
|
92
|
+
manifest = {
|
|
93
|
+
"tool": "aics",
|
|
94
|
+
"version": 1,
|
|
95
|
+
"exportedAt": now_iso(),
|
|
96
|
+
"sourceHost": host(),
|
|
97
|
+
"clients": ["claude", "cursor"],
|
|
98
|
+
"redacted": redacted,
|
|
99
|
+
"assets": asset_files,
|
|
100
|
+
"counts": {
|
|
101
|
+
"claude": {
|
|
102
|
+
"mcp": len(claude["mcpServers"]),
|
|
103
|
+
"plugins": len(claude["plugins"]),
|
|
104
|
+
"skills": len(claude["skills"]),
|
|
105
|
+
"commands": len(claude["commands"]),
|
|
106
|
+
"agents": len(claude["agents"]),
|
|
107
|
+
},
|
|
108
|
+
"cursor": {
|
|
109
|
+
"mcp": len(cursor["mcp"]),
|
|
110
|
+
"rules": len(cursor["rules"]),
|
|
111
|
+
"extensions": len(cursor["extensions"]),
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
(out_dir / "manifest.json").write_text(json.dumps(manifest, indent=2, ensure_ascii=False))
|
|
116
|
+
return manifest
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def make_tar(bundle_dir: Path) -> Path:
|
|
120
|
+
tar_path = bundle_dir.parent / f"{bundle_dir.name}.tar.gz"
|
|
121
|
+
with tarfile.open(tar_path, "w:gz") as tar:
|
|
122
|
+
tar.add(bundle_dir, arcname=bundle_dir.name)
|
|
123
|
+
return tar_path
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def load_manifest(bundle: Path):
|
|
127
|
+
mf = bundle / "manifest.json"
|
|
128
|
+
if not mf.exists():
|
|
129
|
+
return None
|
|
130
|
+
return json.loads(mf.read_text())
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def list_bundle(bundle: Path, color=False) -> str:
|
|
134
|
+
def p(text, *styles):
|
|
135
|
+
return f"\033[{';'.join(_c[s] for s in styles)}m{text}\033[0m" if color else text
|
|
136
|
+
|
|
137
|
+
m = load_manifest(bundle)
|
|
138
|
+
if not m:
|
|
139
|
+
return f"not an aics bundle (no manifest.json): {bundle}"
|
|
140
|
+
out = [p(f"bundle @ {bundle}", "bold", "cyan"), f"exported: {m['exportedAt']} from {m['sourceHost']}"]
|
|
141
|
+
for c, counts in m["counts"].items():
|
|
142
|
+
out.append(f"{c}: " + ", ".join(f"{k}={v}" for k, v in counts.items()))
|
|
143
|
+
out.append(f"assets: {len(m['assets'])} files")
|
|
144
|
+
out.append(f"redacted: {len(m['redacted'])} fields")
|
|
145
|
+
return "\n".join(out) + "\n"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def diff_bundle(bundle: Path, color=False) -> str:
|
|
149
|
+
def p(text, *styles):
|
|
150
|
+
return f"\033[{';'.join(_c[s] for s in styles)}m{text}\033[0m" if color else text
|
|
151
|
+
|
|
152
|
+
m = load_manifest(bundle)
|
|
153
|
+
if not m:
|
|
154
|
+
return "not an aics bundle"
|
|
155
|
+
claude = scan_claude()
|
|
156
|
+
cursor = scan_cursor()
|
|
157
|
+
out = [p("diff (bundle -> local):", "bold", "cyan")]
|
|
158
|
+
bc = m["counts"]["claude"]
|
|
159
|
+
cc = {
|
|
160
|
+
"mcp": len(claude["mcpServers"]),
|
|
161
|
+
"plugins": len(claude["plugins"]),
|
|
162
|
+
"skills": len(claude["skills"]),
|
|
163
|
+
"commands": len(claude["commands"]),
|
|
164
|
+
"agents": len(claude["agents"]),
|
|
165
|
+
}
|
|
166
|
+
for k in bc:
|
|
167
|
+
sign = "==" if bc[k] == cc[k] else ("+local" if cc[k] > bc[k] else "+bundle")
|
|
168
|
+
style = "green" if sign == "==" else "yellow"
|
|
169
|
+
out.append(f" claude.{k}: bundle={bc[k]} local={cc[k]} " + p(sign, style))
|
|
170
|
+
bcur = m["counts"]["cursor"]
|
|
171
|
+
curc = {
|
|
172
|
+
"mcp": len(cursor["mcp"]),
|
|
173
|
+
"rules": len(cursor["rules"]),
|
|
174
|
+
"extensions": len(cursor["extensions"]),
|
|
175
|
+
}
|
|
176
|
+
for k in bcur:
|
|
177
|
+
sign = "==" if bcur[k] == curc[k] else ("+local" if curc[k] > bcur[k] else "+bundle")
|
|
178
|
+
style = "green" if sign == "==" else "yellow"
|
|
179
|
+
out.append(f" cursor.{k}: bundle={bcur[k]} local={curc[k]} " + p(sign, style))
|
|
180
|
+
return "\n".join(out) + "\n"
|