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.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ *.egg-info/
5
+ build/
6
+ .aics-bundle/
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
@@ -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,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -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"