commit 67fb0abf2991e95df308f498db83c4b4dffb3aad Author: 2949984428 Date: Thu Apr 23 16:16:00 2026 +0800 Initial release v0.1.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ccfa141 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# 用户本地设置(含本机 home 路径,不应入库) +data.json + +# macOS +.DS_Store + +# Editor +.vscode/ +.idea/ + +# Node(如果以后改成 TS 编译) +node_modules/ +*.log diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d9b4aab --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +All notable changes to this plugin will be documented here. + +## [0.1.0] - 2026-04-23 + +### 首发版本 + +**核心功能** +- 扫描 Obsidian 内的 Skill 文件夹,一键 symlink 到 18+ Coding 工具(Claude Code / Codex / Cursor / Gemini CLI / Windsurf / Trae 等) +- 双向同步:vault → 工具(安装)+ 工具 → vault(导入) +- 8 格同步状态卡:已同步 / 部分同步 / 未安装 / 待导入 / 指向错误 / 失效链接 / 冲突,每格 hover 显示用户视角的 tooltip +- Skill 列表带搜索 + 折叠 description + +**自动化** +- 定时扫描(默认 5 分钟,可配置) +- 自动安装新 Skill 到所有 Coding 工具(默认开,安全) +- 自动从工具导入新 Skill 到 vault(默认关,需主动开启) +- 启动时同步状态摘要通知 + +**安全护栏** +- 移除只断开链接,不删除源 Skill +- 冲突状态需人工处理,绝不覆盖工具自带的实体 Skill +- 删除按钮二次确认(3 秒确认窗口) + +**配置** +- 添加 Coding 工具弹窗:18 个预设可选 + 自定义路径 +- 设置面板用用户视角语言,避免技术黑话 +- 仅桌面端(移动端 Obsidian 不支持 symlink) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..64e4b1c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Chocolae + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PUBLISH.md b/PUBLISH.md new file mode 100644 index 0000000..5f53a62 --- /dev/null +++ b/PUBLISH.md @@ -0,0 +1,151 @@ +# 发布到 GitHub + Obsidian 插件商店 + +> 本指南假设你已安装 `git` 和 `gh`(GitHub CLI),且已 `gh auth login`。 + +## 一次性准备:替换占位符 + +发布前先把这些 `` 占位符替换成你的真实信息: + +```bash +# 在仓库根目录执行(macOS) +cd ~/Documents/claude-skill-sync +sed -i '' 's//你的真名/g' manifest.json LICENSE +sed -i '' 's//你的GitHub用户名/g' manifest.json README.md docs/USAGE.md +``` + +或者手动编辑: +- `manifest.json` → `author`、`authorUrl` +- `LICENSE` → 版权人姓名 +- `README.md` → repo 链接 +- `docs/USAGE.md` → repo 链接 + +## 第一阶段:发布到 GitHub + +### 1. 初始化 git 仓库 + +```bash +cd ~/Documents/claude-skill-sync +git init +git add . +git status # 确认 data.json 已被 .gitignore 忽略 +git commit -m "Initial release v0.1.0" +``` + +### 2. 在 GitHub 创建公开 repo 并 push + +```bash +gh repo create claude-skill-sync --public --source=. --push --description "Manage AI coding agent Skills from your Obsidian vault" +``` + +或网页创建后: +```bash +git branch -M main +git remote add origin https://github.com/<你的用户名>/claude-skill-sync.git +git push -u origin main +``` + +### 3. 创建 v0.1.0 Release(**Obsidian 商店硬要求**) + +商店审核会从 GitHub Releases 拉取 `manifest.json` / `main.js` / `styles.css`,**tag 名必须等于 manifest.json 的 version,不带 `v` 前缀**。 + +```bash +gh release create 0.1.0 \ + manifest.json main.js styles.css \ + --title "0.1.0 — 首发版本" \ + --notes-file CHANGELOG.md +``` + +注意三个 asset 必须直接上传到 release(不是源码 zip 里)。 + +### 4. 验证 release 可访问 + +```bash +curl -sI https://github.com/<你的用户名>/claude-skill-sync/releases/download/0.1.0/manifest.json | head -1 +# 期望 200 或 302 +``` + +## 第二阶段:提交到 Obsidian 插件商店 + +### 1. Fork obsidianmd/obsidian-releases + +```bash +gh repo fork obsidianmd/obsidian-releases --clone --remote +cd obsidian-releases +``` + +### 2. 编辑 community-plugins.json,追加你的条目 + +打开文件,在数组**末尾**追加(注意前一项末尾要补逗号): + +```json +{ + "id": "claude-skill-sync", + "name": "Claude Skill Sync", + "author": "<你的真名>", + "description": "Manage AI coding agent Skills (Claude Code, Codex, Cursor, Gemini, and more) from your Obsidian vault. One source, symlinked to each tool.", + "repo": "<你的GitHub用户名>/claude-skill-sync" +} +``` + +`repo` 字段格式必须是 `/`,不带 https://。 + +### 3. 提交 PR + +```bash +git checkout -b add-claude-skill-sync +git add community-plugins.json +git commit -m "Add Claude Skill Sync plugin" +git push origin add-claude-skill-sync +gh pr create --repo obsidianmd/obsidian-releases \ + --title "Add Claude Skill Sync" \ + --body "Plugin repo: https://github.com/<你的用户名>/claude-skill-sync + +Manages AI coding agent Skills from Obsidian vault, symlinked to Claude Code / Codex / Cursor / 18+ tools." +``` + +### 4. 等待审核 + +- 自动检查(bot):通常几分钟内完成,会跑 `manifest.json` 校验、文件存在性、命名规范等 +- 人工审核:1-4 周(受 Obsidian 团队的处理速度影响) +- 如果 bot 标红,修复后 push 同分支即可重新触发 + +## 常见审核问题与修复 + +| 问题 | 原因 | 修复 | +|---|---|---| +| Release tag 不匹配 | tag 用了 `v0.1.0` 而非 `0.1.0` | `gh release delete v0.1.0` 后用正确 tag 重发 | +| manifest.json not found in release | 上传到 release 时没附 asset | `gh release upload 0.1.0 manifest.json main.js styles.css` | +| `id` 已被占用 | 跟现有插件 id 冲突 | 改一个独特的 id,记得 manifest.json 和商店 PR 都改 | +| 包含 `console.log` | 商店要求 production 代码无调试输出 | 全文搜 `console.` 删除或注释 | +| `data.json` 被提交 | `.gitignore` 没生效 | `git rm --cached data.json && git commit` | +| Description 太长 | > 250 chars 会被警告 | 精简到一句话 | +| innerHTML 用法 | Obsidian 出于 XSS 安全禁止 `innerHTML` | 改用 createEl / setText | + +## 后续版本发布流程 + +```bash +# 1. 改 manifest.json 的 version (如 0.2.0) +# 2. versions.json 加新条目: "0.2.0": "1.4.0" +# 3. CHANGELOG.md 加新版本说明 +# 4. 提交并打 tag +git add manifest.json versions.json CHANGELOG.md +git commit -m "Release v0.2.0" +git push + +# 5. 创建新 release +gh release create 0.2.0 manifest.json main.js styles.css \ + --title "0.2.0 — 描述" \ + --notes "见 CHANGELOG.md" +``` + +新版本发布后**不需要再提交 PR 到 obsidian-releases**,商店会自动从你的 GitHub Releases 拉新版。 + +## 用户安装方式 + +商店审核通过后,用户可以: +1. Obsidian → 设置 → 第三方插件 → 浏览 +2. 搜索 "Claude Skill Sync" +3. Install → Enable + +也可以用 [BRAT](https://github.com/TfTHacker/obsidian42-brat) 提前预览(无需等审核): +- 在 BRAT 里 Add Beta Plugin → 输入 `<你的用户名>/claude-skill-sync` diff --git a/README.md b/README.md new file mode 100644 index 0000000..985a9ff --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# Claude Skill Sync + +Obsidian 插件 — 把 Obsidian vault 作为 AI agent skill 的中央仓库,一键 symlink 到 Claude Code / Codex / Cursor 等工具的 skill 目录,跨电脑同步交给 vault 自身的 sync 方案。 + +> 配套理念:**单一真身(vault),多处替身(symlink)**。任何位置编辑都即时一致,不复制内容、不占额外空间。 + +![架构图](docs/architecture.png) + +> 详细使用指南:[docs/USAGE.md](docs/USAGE.md) + +## 安装 + +### 方式 A:开发者本地(推荐) + +```bash +# 1. clone 到本地任意位置 +git clone https://github.com//claude-skill-sync.git ~/Documents/claude-skill-sync + +# 2. 在你的 Obsidian vault 里建 symlink 指向它 +cd /path/to/your-vault +ln -s ~/Documents/claude-skill-sync .obsidian/plugins/claude-skill-sync + +# 3. 启动 Obsidian → 设置 → 第三方插件 → 关闭安全模式 → 启用 Claude Skill Sync +``` + +升级:`cd ~/Documents/claude-skill-sync && git pull`,然后在 Obsidian 里重新加载插件即可。 + +### 方式 B:直接复制(最简) + +把整个目录复制到 vault 内: + +```bash +git clone https://github.com//claude-skill-sync.git \ + /path/to/your-vault/.obsidian/plugins/claude-skill-sync +``` + +## 启用后 + +1. 设置 → Claude Skill Sync 中确认: + - **Vault 内 Skill 根目录**(默认 `Skills`,可改成你 vault 里的实际路径,如 `SkillPack/skills`) + - **目标平台**(默认 Claude Code + Codex/Agents,可加任意) +2. 左侧 ribbon 点插头图标打开侧边栏 +3. 在 vault 的 skill 根目录下放 skill(每个子目录 = 一个 skill,含 `SKILL.md`) +4. 点"全部安装"——所有 skill symlink 到本机平台目录,AI 工具立即可用 + +## 功能 + +- **侧边栏状态卡**:8 格 grid 显示已同步 / 部分同步 / 未安装 / 待导入 / 指向错误 / 失效 link / 冲突 +- **双向同步**:vault → 平台(install)+ 平台 → vault(import 向导) +- **失效 link 一键清理**:vault 删了 skill,残留的 symlink 一键扫掉 +- **指向错误一键修复**:vault 路径变了,所有 symlink 一键改指 +- **定时自动同步**(默认 5 分钟):vault 新 skill 自动安装;平台新真目录默认仅提醒、可选自动导入 +- **启动状态提醒**:打开 Obsidian 弹通知摘要 + +## 工作机制 + +``` +vault/// ← 真身(数据) + ▲ + │ symlink + │ +~/.claude/skills/ ← 替身 +~/.agents/skills/ ← 替身 +~/.cursor/skills/ ← 替身(自行添加平台) +``` + +所有 AI 工具读取的是同一份内容,跨工具立即一致。 + +## 跨电脑同步 + +vault 通过 Obsidian Sync / iCloud / Git / 坚果云等任意方案同步真身;每台电脑上的"替身"由本插件本地建立。 + +`data.json`(每机平台路径)应当**不参与跨机同步**(含 `~`,不同电脑展开后不同;本仓库 `.gitignore` 已忽略)。 + +## 平台支持 + +- ✅ macOS / Linux / Windows 桌面端 +- ❌ 移动端(iOS/Android Obsidian 无 Node fs / symlink 能力,插件 `isDesktopOnly: true`) + +## 配置示例 + +`data.json`(首次启用后自动生成;勿提交至 git): + +```json +{ + "skillRoot": "SkillPack/skills", + "platforms": [ + { "name": "Claude Code", "target": "~/.claude/skills", "enabled": true }, + { "name": "Codex / Agents", "target": "~/.agents/skills", "enabled": true }, + { "name": "Cursor", "target": "~/.cursor/skills", "enabled": false } + ], + "notifyOnStartup": true, + "autoScanIntervalMin": 5, + "autoInstall": true, + "autoImport": false +} +``` + +## 开发 + +纯 JS,无 build。改 `main.js` → Obsidian 设置里关掉再开启插件即可。 + +``` +. +├── manifest.json # 插件元信息 +├── main.js # 全部逻辑(~770 行) +├── styles.css # 侧边栏样式 +├── data.json # 用户本地设置(gitignore) +└── README.md +``` + +## License + +MIT — 见 LICENSE diff --git a/docs/USAGE.md b/docs/USAGE.md new file mode 100644 index 0000000..7ad4c9b --- /dev/null +++ b/docs/USAGE.md @@ -0,0 +1,164 @@ +# Claude Skill Sync — 使用指南 + +> Obsidian 插件 + AI Coding 工具的协同管理:把 vault 当中央仓库,所有 AI 工具读到的是同一份 skill。 + +## 架构图 + +![架构图](./architecture.png) + +> 源文件:[architecture.excalidraw](./architecture.excalidraw) +> 在 [excalidraw.com](https://excalidraw.com) 打开 → Open → 选择该文件即可编辑。 +> 导出 PNG:在 excalidraw.com 里 Menu → Export image → PNG,覆盖 `architecture.png` 后 commit 即可在 README 里渲染。 + +## 30 秒理解 + +- vault 内 `SkillPack/skills//` 是 skill **真身**(数据本体,唯一一份) +- `~/.claude/skills/` `~/.agents/skills/` 等是 **替身**(symlink,指向真身) +- 跨电脑:vault 同步把真身推到另一台机;另一台机点插件「全部安装」自动建好替身 +- 任何位置编辑都即时一致 — Obsidian 改、Claude Code 立刻看到 + +## 安装(4 步) + +### 1. clone 插件 repo + +```bash +git clone https://github.com//claude-skill-sync.git ~/Documents/claude-skill-sync +``` + +### 2. 在 vault 里建 skill 中央目录 + +```bash +mkdir -p /path/to/your-vault/SkillPack/skills +``` + +> 目录名可改,默认是 `Skills/`,本指南推荐用 `SkillPack/skills/`(便于和文档放一起)。 + +### 3. 把插件 symlink 到 vault 的 .obsidian/plugins/ + +```bash +cd /path/to/your-vault +ln -s ~/Documents/claude-skill-sync .obsidian/plugins/claude-skill-sync +``` + +> 用 symlink 而不是 cp:之后 `git pull` 升级插件,所有 vault 一起生效。 + +### 4. 在 Obsidian 启用 + +1. 设置 → 第三方插件 → **关闭安全模式** +2. 启用 **Claude Skill Sync** +3. 设置面板里把 **Vault 内 Skill 根目录**改成你的实际路径(如 `SkillPack/skills`) + +## 日常使用 + +### 添加一个新 skill + +```bash +mkdir -p /path/to/your-vault/SkillPack/skills/my-new-skill +``` + +在该目录里创建 `SKILL.md`: + +```markdown +--- +name: my-new-skill +description: 一句话描述这个 skill 干嘛 +--- + +# my-new-skill + +详细内容... +``` + +回到 Obsidian 侧边栏点「全部安装」(或等 5 分钟自动同步),所有 AI 工具立刻可用。 + +### 添加新平台 + +设置 → Claude Skill Sync → 「+ 添加平台」按钮,弹窗里选预设: + +- Claude Code, Codex / Agents, Cursor, Gemini CLI, Qwen Code, Windsurf, Trae / Trae CN +- Junie, Factory Droid, Augment, OpenCode, KiloCode, Qoder, OB1, Amp, Kiro, CodeBuddy +- 自定义(点底部「+ 自定义平台」展开) + +### 跨电脑同步 + +**新机首次接入**: +1. vault 同步软件(Obsidian Sync / iCloud / Git / 坚果云 / Remotely Save)拉下 vault +2. 按上面安装步骤 1+3+4(不需要重新建 SkillPack/skills/,sync 已经带过来了) +3. 启用插件后状态卡显示「48 未安装」,点「全部安装」即完成 + +**日常**: +- A 电脑添加新 skill → vault sync 推到云 → B 电脑拉到 → B 电脑插件 5 分钟内自动安装(默认开启) + +## 侧边栏功能 + +**顶部**: +- 「刷新」「全部安装」「导入现有」三个按钮 +- 中央目录路径显示 + +**同步状态卡**(8 格 grid): +| 格子 | 含义 | 点击行为 | +|---|---|---| +| vault skill (×N 平台) | vault 内 skill 数 | — | +| 已同步 | 在所有启用平台都装好的数 | — | +| 部分同步 | 只装了部分平台 | 全部安装补齐 | +| 未安装 | 一个平台都没装 | 全部安装 | +| 待导入 | 平台目录有真目录、vault 无 | 开导入向导 | +| 指向错误 | symlink 指向其他位置 | 修复全部 | +| 失效 link | symlink 指向已不存在 | 一键清理 | +| 冲突 | 平台目录有同名真目录 | 需手动处理 | + +**搜索框**:按 skill 名 / description 即时过滤 + +**skill 列表**(默认收起 description): +- 每行:`▸ skill-name` + 各平台徽章 + 操作按钮(安装 / 移除 / 改指向) +- 点 ▸ 三角或 name 行展开 description + +## 设置项详解 + +### 启动时提醒同步状态(默认 ON) +打开 Obsidian 后 1.5 秒扫一遍,发现不一致弹通知。 + +### 定时扫描间隔(默认 5 分钟,0 = 关) +后台轮询。修改即时生效。 + +### 自动安装新 skill (vault → 平台)(默认 ON,安全) +扫描时发现 vault 新 skill 没装到平台 → 自动建 symlink。 + +### 自动导入新 skill (平台 → vault)(默认 OFF,⚠️ 危险) +扫描时发现平台目录有新真目录 → 自动 mv 进 vault。 +**mv 不可逆**,仅当确定平台目录不会被其他工具临时使用时才开。 + +## 安全护栏 + +| 行为 | 保护 | +|---|---| +| 卸载 / 移除 | 只删 symlink,检测目标不是 symlink 拒绝执行 | +| 全部安装 | 跳过冲突状态(同名真目录),不会覆盖 | +| 导入现有 | 用 `fs.rename` 原子操作,跨设备失败立刻报告 | +| 清理失效 | 只删孤儿 symlink,不动真目录 | +| 自动同步 | install 默认开(安全),import 默认关(危险) | + +## 常见问题 + +**Q:状态卡数字不对?** +A:点「刷新」或在设置里修正 skillRoot 路径。 + +**Q:换电脑后所有 symlink 都「指向错误」?** +A:vault 路径变了。点「指向错误」格子一键 repair,自动按当前 vault 路径重建。 + +**Q:移动设备能用吗?** +A:不能。iOS/Android Obsidian 没有 Node fs / symlink 能力,插件 `isDesktopOnly: true`。但 vault 里的 skill 文档你仍然可以在手机上**阅读和编辑**,回到桌面端再 install。 + +**Q:插件升级怎么做?** +A:`cd ~/Documents/claude-skill-sync && git pull`,回 Obsidian 设置里关掉再开启 Claude Skill Sync 即重载。 + +**Q:data.json 要不要提交 git?** +A:**不要**。它含本机 home 路径,每台电脑展开后不同;本仓库 `.gitignore` 已忽略。 + +**Q:vault 同步把 .obsidian/ 排除了,插件怎么办?** +A:插件本来就是每台电脑独立 `git clone` + `ln -s`。vault 同步只负责真身(`SkillPack/skills/`)。 + +## 反馈 + +- Issue: +- 详细原理:[../docs/流程与原理.md](https://github.com/<owner>/skill-pack-data) (如果你也共享了 SkillPack 数据 repo) diff --git a/docs/architecture.excalidraw b/docs/architecture.excalidraw new file mode 100644 index 0000000..c3f0f33 --- /dev/null +++ b/docs/architecture.excalidraw @@ -0,0 +1,817 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "title-main", + "type": "text", + "x": 380, "y": 20, "width": 540, "height": 38, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100001, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "Claude Skill Sync — 架构与使用流程", + "fontSize": 28, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "Claude Skill Sync — 架构与使用流程", + "autoResize": true, "lineHeight": 1.25 + }, + + { + "id": "github-container", + "type": "rectangle", + "x": 50, "y": 90, "width": 280, "height": 400, + "angle": 0, + "strokeColor": "#15803d", "backgroundColor": "#d3f9d8", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 3}, "seed": 100002, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false + }, + { + "id": "github-title", + "type": "text", + "x": 110, "y": 105, "width": 160, "height": 28, + "angle": 0, + "strokeColor": "#15803d", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100003, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "GitHub Repo", + "fontSize": 20, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "GitHub Repo", + "autoResize": true, "lineHeight": 1.25 + }, + { + "id": "github-sub", + "type": "text", + "x": 95, "y": 138, "width": 200, "height": 22, + "angle": 0, + "strokeColor": "#374151", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100004, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "claude-skill-sync", + "fontSize": 14, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "claude-skill-sync", + "autoResize": true, "lineHeight": 1.25 + }, + { + "id": "github-files", + "type": "text", + "x": 80, "y": 180, "width": 220, "height": 220, + "angle": 0, + "strokeColor": "#374151", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100005, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "• main.js\n• manifest.json\n• styles.css\n• README.md\n• LICENSE\n• .gitignore", + "fontSize": 16, "fontFamily": 5, + "textAlign": "left", "verticalAlign": "top", + "containerId": null, + "originalText": "• main.js\n• manifest.json\n• styles.css\n• README.md\n• LICENSE\n• .gitignore", + "autoResize": true, "lineHeight": 1.5 + }, + { + "id": "github-note", + "type": "text", + "x": 75, "y": 440, "width": 230, "height": 22, + "angle": 0, + "strokeColor": "#15803d", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100006, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "纯 JS · 无 build · MIT", + "fontSize": 14, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "纯 JS · 无 build · MIT", + "autoResize": true, "lineHeight": 1.25 + }, + + { + "id": "vault-container", + "type": "rectangle", + "x": 380, "y": 90, "width": 560, "height": 400, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "#a5d8ff", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 3}, "seed": 100010, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false + }, + { + "id": "vault-title", + "type": "text", + "x": 575, "y": 105, "width": 170, "height": 28, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100011, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "Obsidian Vault", + "fontSize": 20, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "Obsidian Vault", + "autoResize": true, "lineHeight": 1.25 + }, + { + "id": "vault-sub", + "type": "text", + "x": 530, "y": 138, "width": 260, "height": 22, + "angle": 0, + "strokeColor": "#374151", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100012, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "跨电脑通过 vault 自身同步", + "fontSize": 14, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "跨电脑通过 vault 自身同步", + "autoResize": true, "lineHeight": 1.25 + }, + + { + "id": "skills-box", + "type": "rectangle", + "x": 400, "y": 175, "width": 520, "height": 115, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "#ffffff", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 3}, "seed": 100020, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false + }, + { + "id": "skills-title", + "type": "text", + "x": 415, "y": 187, "width": 280, "height": 22, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100021, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "SkillPack/skills/ 「真身, 唯一一份」", + "fontSize": 16, "fontFamily": 5, + "textAlign": "left", "verticalAlign": "middle", + "containerId": null, + "originalText": "SkillPack/skills/ 「真身, 唯一一份」", + "autoResize": true, "lineHeight": 1.25 + }, + { + "id": "skills-list", + "type": "text", + "x": 415, "y": 218, "width": 490, "height": 60, + "angle": 0, + "strokeColor": "#374151", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100022, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "├─ hello-world/ ├─ lark-im/ ├─ ai-products/\n├─ agent-browser/ ├─ pm-tracker/ ├─ research/\n└─ ... 「48 个 skill」", + "fontSize": 14, "fontFamily": 5, + "textAlign": "left", "verticalAlign": "top", + "containerId": null, + "originalText": "├─ hello-world/ ├─ lark-im/ ├─ ai-products/\n├─ agent-browser/ ├─ pm-tracker/ ├─ research/\n└─ ... 「48 个 skill」", + "autoResize": true, "lineHeight": 1.4 + }, + + { + "id": "docs-box", + "type": "rectangle", + "x": 400, "y": 305, "width": 250, "height": 80, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "#ffffff", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 3}, "seed": 100030, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false + }, + { + "id": "docs-text", + "type": "text", + "x": 415, "y": 318, "width": 220, "height": 56, + "angle": 0, + "strokeColor": "#374151", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100031, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "SkillPack/docs/\n流程·实现·共享文档", + "fontSize": 15, "fontFamily": 5, + "textAlign": "left", "verticalAlign": "top", + "containerId": null, + "originalText": "SkillPack/docs/\n流程·实现·共享文档", + "autoResize": true, "lineHeight": 1.4 + }, + + { + "id": "plugin-box", + "type": "rectangle", + "x": 670, "y": 305, "width": 250, "height": 80, + "angle": 0, + "strokeColor": "#f59e0b", "backgroundColor": "#fff3bf", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "dashed", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 3}, "seed": 100040, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false + }, + { + "id": "plugin-text", + "type": "text", + "x": 685, "y": 318, "width": 220, "height": 56, + "angle": 0, + "strokeColor": "#374151", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100041, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": ".obsidian/plugins/\nclaude-skill-sync 「symlink」", + "fontSize": 14, "fontFamily": 5, + "textAlign": "left", "verticalAlign": "top", + "containerId": null, + "originalText": ".obsidian/plugins/\nclaude-skill-sync 「symlink」", + "autoResize": true, "lineHeight": 1.4 + }, + + { + "id": "vault-bottom-note", + "type": "text", + "x": 405, "y": 410, "width": 510, "height": 22, + "angle": 0, + "strokeColor": "#3b82f6", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100050, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "Obsidian Sync · iCloud · Git · 坚果云 · Remotely Save", + "fontSize": 13, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "Obsidian Sync · iCloud · Git · 坚果云 · Remotely Save", + "autoResize": true, "lineHeight": 1.25 + }, + { + "id": "vault-bottom-note2", + "type": "text", + "x": 480, "y": 442, "width": 360, "height": 22, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100051, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "↑ vault 跨电脑同步任选其一", + "fontSize": 13, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "↑ vault 跨电脑同步任选其一", + "autoResize": true, "lineHeight": 1.25 + }, + + { + "id": "tools-container", + "type": "rectangle", + "x": 990, "y": 90, "width": 380, "height": 400, + "angle": 0, + "strokeColor": "#d97706", "backgroundColor": "#ffd8a8", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 3}, "seed": 100060, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false + }, + { + "id": "tools-title", + "type": "text", + "x": 1095, "y": 105, "width": 170, "height": 28, + "angle": 0, + "strokeColor": "#d97706", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100061, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "AI Coding Tools", + "fontSize": 20, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "AI Coding Tools", + "autoResize": true, "lineHeight": 1.25 + }, + { + "id": "tools-sub", + "type": "text", + "x": 1060, "y": 138, "width": 240, "height": 22, + "angle": 0, + "strokeColor": "#374151", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100062, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "本地各工具的 skills 目录", + "fontSize": 13, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "本地各工具的 skills 目录", + "autoResize": true, "lineHeight": 1.25 + }, + + { + "id": "tool1-box", + "type": "rectangle", + "x": 1010, "y": 175, "width": 340, "height": 80, + "angle": 0, + "strokeColor": "#d97706", "backgroundColor": "#ffffff", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 3}, "seed": 100070, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false + }, + { + "id": "tool1-text", + "type": "text", + "x": 1025, "y": 188, "width": 310, "height": 56, + "angle": 0, + "strokeColor": "#374151", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100071, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "Claude Code\n~/.claude/skills/", + "fontSize": 16, "fontFamily": 5, + "textAlign": "left", "verticalAlign": "top", + "containerId": null, + "originalText": "Claude Code\n~/.claude/skills/", + "autoResize": true, "lineHeight": 1.4 + }, + + { + "id": "tool2-box", + "type": "rectangle", + "x": 1010, "y": 270, "width": 340, "height": 80, + "angle": 0, + "strokeColor": "#d97706", "backgroundColor": "#ffffff", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 3}, "seed": 100080, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false + }, + { + "id": "tool2-text", + "type": "text", + "x": 1025, "y": 283, "width": 310, "height": 56, + "angle": 0, + "strokeColor": "#374151", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100081, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "Codex / Agents\n~/.agents/skills/", + "fontSize": 16, "fontFamily": 5, + "textAlign": "left", "verticalAlign": "top", + "containerId": null, + "originalText": "Codex / Agents\n~/.agents/skills/", + "autoResize": true, "lineHeight": 1.4 + }, + + { + "id": "tool3-box", + "type": "rectangle", + "x": 1010, "y": 365, "width": 340, "height": 100, + "angle": 0, + "strokeColor": "#d97706", "backgroundColor": "#ffffff", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 3}, "seed": 100090, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false + }, + { + "id": "tool3-text", + "type": "text", + "x": 1025, "y": 378, "width": 310, "height": 76, + "angle": 0, + "strokeColor": "#374151", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100091, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "Cursor / Gemini / Trae / ...\n~/.cursor/skills/\n~/.gemini/skills/ (18 个预设)", + "fontSize": 14, "fontFamily": 5, + "textAlign": "left", "verticalAlign": "top", + "containerId": null, + "originalText": "Cursor / Gemini / Trae / ...\n~/.cursor/skills/\n~/.gemini/skills/ (18 个预设)", + "autoResize": true, "lineHeight": 1.4 + }, + + { + "id": "arrow-github-plugin", + "type": "arrow", + "x": 330, "y": 345, "width": 340, "height": 0, + "angle": 0, + "strokeColor": "#f59e0b", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "dashed", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 2}, "seed": 100100, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "points": [[0, 0], [340, 0]], + "lastCommittedPoint": null, "startBinding": null, "endBinding": null, + "startArrowhead": null, "endArrowhead": "arrow" + }, + { + "id": "arrow-github-plugin-label", + "type": "text", + "x": 380, "y": 318, "width": 240, "height": 22, + "angle": 0, + "strokeColor": "#d97706", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100101, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "git clone + ln -s", + "fontSize": 13, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "git clone + ln -s", + "autoResize": true, "lineHeight": 1.25 + }, + + { + "id": "arrow-skills-tool1", + "type": "arrow", + "x": 920, "y": 215, "width": 90, "height": 0, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 2}, "seed": 100110, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "points": [[0, 0], [90, 0]], + "lastCommittedPoint": null, "startBinding": null, "endBinding": null, + "startArrowhead": null, "endArrowhead": "arrow" + }, + { + "id": "arrow-skills-tool2", + "type": "arrow", + "x": 920, "y": 230, "width": 90, "height": 80, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 2}, "seed": 100111, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "points": [[0, 0], [90, 80]], + "lastCommittedPoint": null, "startBinding": null, "endBinding": null, + "startArrowhead": null, "endArrowhead": "arrow" + }, + { + "id": "arrow-skills-tool3", + "type": "arrow", + "x": 920, "y": 245, "width": 90, "height": 175, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 2}, "seed": 100112, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "points": [[0, 0], [90, 175]], + "lastCommittedPoint": null, "startBinding": null, "endBinding": null, + "startArrowhead": null, "endArrowhead": "arrow" + }, + { + "id": "symlink-label", + "type": "text", + "x": 935, "y": 165, "width": 70, "height": 22, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100113, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "symlink", + "fontSize": 13, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "symlink", + "autoResize": true, "lineHeight": 1.25 + }, + + { + "id": "flow-title", + "type": "text", + "x": 580, "y": 540, "width": 240, "height": 32, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100200, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "使用流程「6 步」", + "fontSize": 24, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "使用流程「6 步」", + "autoResize": true, "lineHeight": 1.25 + }, + + { + "id": "step1-box", + "type": "rectangle", + "x": 50, "y": 600, "width": 200, "height": 100, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "#a5d8ff", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 3}, "seed": 100210, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false + }, + { + "id": "step1-text", + "type": "text", + "x": 65, "y": 615, "width": 170, "height": 70, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100211, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "1. clone\n插件 GitHub repo\n到本地", + "fontSize": 15, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "1. clone\n插件 GitHub repo\n到本地", + "autoResize": true, "lineHeight": 1.4 + }, + + { + "id": "step2-box", + "type": "rectangle", + "x": 280, "y": 600, "width": 200, "height": 100, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "#a5d8ff", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 3}, "seed": 100220, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false + }, + { + "id": "step2-text", + "type": "text", + "x": 295, "y": 615, "width": 170, "height": 70, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100221, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "2. 在 vault 建\nSkillPack/skills/\n目录", + "fontSize": 15, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "2. 在 vault 建\nSkillPack/skills/\n目录", + "autoResize": true, "lineHeight": 1.4 + }, + + { + "id": "step3-box", + "type": "rectangle", + "x": 510, "y": 600, "width": 200, "height": 100, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "#a5d8ff", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 3}, "seed": 100230, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false + }, + { + "id": "step3-text", + "type": "text", + "x": 525, "y": 615, "width": 170, "height": 70, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100231, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "3. ln -s 插件\n到 .obsidian/\nplugins/", + "fontSize": 15, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "3. ln -s 插件\n到 .obsidian/\nplugins/", + "autoResize": true, "lineHeight": 1.4 + }, + + { + "id": "step4-box", + "type": "rectangle", + "x": 740, "y": 600, "width": 200, "height": 100, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "#a5d8ff", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 3}, "seed": 100240, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false + }, + { + "id": "step4-text", + "type": "text", + "x": 755, "y": 615, "width": 170, "height": 70, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100241, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "4. Obsidian\n启用插件\n「关安全模式」", + "fontSize": 15, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "4. Obsidian\n启用插件\n「关安全模式」", + "autoResize": true, "lineHeight": 1.4 + }, + + { + "id": "step5-box", + "type": "rectangle", + "x": 970, "y": 600, "width": 200, "height": 100, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "#b2f2bb", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 3}, "seed": 100250, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false + }, + { + "id": "step5-text", + "type": "text", + "x": 985, "y": 615, "width": 170, "height": 70, + "angle": 0, + "strokeColor": "#15803d", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100251, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "5. 在 vault\n添加 / 编辑 skill\n「即时一致」", + "fontSize": 15, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "5. 在 vault\n添加 / 编辑 skill\n「即时一致」", + "autoResize": true, "lineHeight": 1.4 + }, + + { + "id": "step6-box", + "type": "rectangle", + "x": 1200, "y": 600, "width": 200, "height": 100, + "angle": 0, + "strokeColor": "#15803d", "backgroundColor": "#b2f2bb", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 3}, "seed": 100260, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false + }, + { + "id": "step6-text", + "type": "text", + "x": 1215, "y": 615, "width": 170, "height": 70, + "angle": 0, + "strokeColor": "#15803d", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100261, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "6. 点「全部安装」\nAI 工具立即可用\n✓ 完成", + "fontSize": 15, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "6. 点「全部安装」\nAI 工具立即可用\n✓ 完成", + "autoResize": true, "lineHeight": 1.4 + }, + + { + "id": "flow-arrow1", + "type": "arrow", + "x": 250, "y": 650, "width": 30, "height": 0, + "angle": 0, + "strokeColor": "#3b82f6", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 2}, "seed": 100300, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "points": [[0, 0], [30, 0]], + "lastCommittedPoint": null, "startBinding": null, "endBinding": null, + "startArrowhead": null, "endArrowhead": "arrow" + }, + { + "id": "flow-arrow2", + "type": "arrow", + "x": 480, "y": 650, "width": 30, "height": 0, + "angle": 0, + "strokeColor": "#3b82f6", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 2}, "seed": 100301, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "points": [[0, 0], [30, 0]], + "lastCommittedPoint": null, "startBinding": null, "endBinding": null, + "startArrowhead": null, "endArrowhead": "arrow" + }, + { + "id": "flow-arrow3", + "type": "arrow", + "x": 710, "y": 650, "width": 30, "height": 0, + "angle": 0, + "strokeColor": "#3b82f6", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 2}, "seed": 100302, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "points": [[0, 0], [30, 0]], + "lastCommittedPoint": null, "startBinding": null, "endBinding": null, + "startArrowhead": null, "endArrowhead": "arrow" + }, + { + "id": "flow-arrow4", + "type": "arrow", + "x": 940, "y": 650, "width": 30, "height": 0, + "angle": 0, + "strokeColor": "#3b82f6", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 2}, "seed": 100303, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "points": [[0, 0], [30, 0]], + "lastCommittedPoint": null, "startBinding": null, "endBinding": null, + "startArrowhead": null, "endArrowhead": "arrow" + }, + { + "id": "flow-arrow5", + "type": "arrow", + "x": 1170, "y": 650, "width": 30, "height": 0, + "angle": 0, + "strokeColor": "#15803d", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": {"type": 2}, "seed": 100304, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "points": [[0, 0], [30, 0]], + "lastCommittedPoint": null, "startBinding": null, "endBinding": null, + "startArrowhead": null, "endArrowhead": "arrow" + }, + + { + "id": "bottom-tagline", + "type": "text", + "x": 280, "y": 750, "width": 880, "height": 28, + "angle": 0, + "strokeColor": "#1e40af", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100400, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "单一真身「vault」 · 多处替身「symlink」 · 跨电脑同步交给 vault 自身", + "fontSize": 18, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "单一真身「vault」 · 多处替身「symlink」 · 跨电脑同步交给 vault 自身", + "autoResize": true, "lineHeight": 1.25 + }, + { + "id": "bottom-tagline2", + "type": "text", + "x": 380, "y": 790, "width": 680, "height": 22, + "angle": 0, + "strokeColor": "#3b82f6", "backgroundColor": "transparent", + "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", + "roughness": 1, "opacity": 100, "groupIds": [], + "roundness": null, "seed": 100401, "version": 1, + "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false, + "text": "任何位置编辑都即时一致 · 不复制内容 · 不占额外空间", + "fontSize": 14, "fontFamily": 5, + "textAlign": "center", "verticalAlign": "middle", + "containerId": null, + "originalText": "任何位置编辑都即时一致 · 不复制内容 · 不占额外空间", + "autoResize": true, "lineHeight": 1.25 + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} diff --git a/main.js b/main.js new file mode 100644 index 0000000..ff2d796 --- /dev/null +++ b/main.js @@ -0,0 +1,1010 @@ +'use strict'; + +const obsidian = require('obsidian'); +const fs = require('fs'); +const fsp = fs.promises; +const path = require('path'); +const os = require('os'); + +const VIEW_TYPE = 'claude-skill-sync-view'; + +const PRESET_PLATFORMS = [ + { name: 'Claude Code', target: '~/.claude/skills' }, + { name: 'Codex / Agents', target: '~/.agents/skills' }, + { name: 'Cursor', target: '~/.cursor/skills' }, + { name: 'Gemini CLI', target: '~/.gemini/skills' }, + { name: 'Qwen Code', target: '~/.qwen/skills' }, + { name: 'Windsurf', target: '~/.windsurf/skills' }, + { name: 'Trae', target: '~/.trae/skills' }, + { name: 'Trae CN', target: '~/.trae-cn/skills' }, + { name: 'Junie', target: '~/.junie/skills' }, + { name: 'Factory Droid', target: '~/.factory/skills' }, + { name: 'Augment', target: '~/.augment/skills' }, + { name: 'OpenCode', target: '~/.opencode/skills' }, + { name: 'KiloCode', target: '~/.kilocode/skills' }, + { name: 'Qoder', target: '~/.qoder/skills' }, + { name: 'OB1', target: '~/.ob1/skills' }, + { name: 'Amp', target: '~/.amp/skills' }, + { name: 'Kiro', target: '~/.kiro/skills' }, + { name: 'CodeBuddy', target: '~/.codebuddy/skills' } +]; + +const DEFAULT_SETTINGS = { + skillRoot: 'Skills', + platforms: [ + { name: 'Claude Code', target: '~/.claude/skills', enabled: true }, + { name: 'Codex / Agents', target: '~/.agents/skills', enabled: true } + ], + notifyOnStartup: true, + autoScanIntervalMin: 5, + autoInstall: true, + autoImport: false +}; + +function expandHome(p) { + if (!p) return p; + if (p === '~') return os.homedir(); + if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2)); + return p; +} + +function vaultBasePath(app) { + const a = app.vault.adapter; + return a.basePath || (typeof a.getBasePath === 'function' ? a.getBasePath() : ''); +} + +async function lstatSafe(p) { + try { return await fsp.lstat(p); } catch { return null; } +} +async function exists(p) { + return (await lstatSafe(p)) !== null; +} +async function isSymlink(p) { + const st = await lstatSafe(p); + return st ? st.isSymbolicLink() : false; +} +async function readlinkSafe(p) { + try { return await fsp.readlink(p); } catch { return null; } +} + +class SkillSyncPlugin extends obsidian.Plugin { + async onload() { + await this.loadSettings(); + this.addSettingTab(new SkillSyncSettingTab(this.app, this)); + this.registerView(VIEW_TYPE, (leaf) => new SkillSyncView(leaf, this)); + + this.addRibbonIcon('plug', 'Skill Sync', () => this.activateView()); + this.addCommand({ + id: 'open-skill-sync', + name: '打开 Skill Sync 侧边栏', + callback: () => this.activateView() + }); + this.addCommand({ + id: 'refresh-skills', + name: '刷新 Skill 列表', + callback: () => this.refreshAllViews() + }); + this.addCommand({ + id: 'install-all', + name: '把所有 Skill 安装到所有 Coding 工具', + callback: () => this.installAll() + }); + this.addCommand({ + id: 'import-existing', + name: '扫描并导入 Coding 工具里已有的 Skill', + callback: () => new ImportModal(this.app, this).open() + }); + this.addCommand({ + id: 'cleanup-dangling', + name: '清理失效链接', + callback: () => this.cleanupDangling() + }); + this.addCommand({ + id: 'repair-mislinked', + name: '修复指向错误的链接', + callback: () => this.repairAllMislinked() + }); + + this.app.workspace.onLayoutReady(() => { + setTimeout(() => this.checkAndNotifyOnStartup(), 1500); + this.startAutoScan(); + }); + } + + onunload() { + this.stopAutoScan(); + this.app.workspace.detachLeavesOfType(VIEW_TYPE); + } + + startAutoScan() { + this.stopAutoScan(); + const min = Number(this.settings.autoScanIntervalMin) || 0; + if (min <= 0) return; + this._scanTimer = setInterval(() => this.autoScanTick(), min * 60 * 1000); + } + + stopAutoScan() { + if (this._scanTimer) { + clearInterval(this._scanTimer); + this._scanTimer = null; + } + } + + async autoScanTick() { + let installedN = 0, importedN = 0; + if (this.settings.autoInstall) { + const skills = await this.listSkills(); + for (const s of skills) { + for (const p of this.settings.platforms) { + if (!p.enabled) continue; + const st = await this.getStatus(s, p); + if (st.state === 'missing') { + if (await this.install(s, p)) installedN++; + } + } + } + } + if (this.settings.autoImport) { + const items = await this.listImportable(); + for (const it of items) { + if (await this.importSkill(it)) importedN++; + } + } + if (installedN > 0 || importedN > 0) { + const parts = []; + if (installedN > 0) parts.push(`安装 ${installedN}`); + if (importedN > 0) parts.push(`导入 ${importedN}`); + new obsidian.Notice(`Skill Sync 自动同步:${parts.join(' / ')}`, 6000); + this.refreshAllViews(); + } else if (this.settings.notifyOnStartup) { + const s = await this.getSyncSummary(); + if (s.hasIssue) { + const parts = []; + if (s.notInstalled > 0) parts.push(`${s.notInstalled} 未安装`); + if (s.partiallySynced > 0) parts.push(`${s.partiallySynced} 部分同步`); + if (s.importableCount > 0) parts.push(`${s.importableCount} 待导入`); + if (parts.length > 0) { + new obsidian.Notice(`Skill Sync 状态:${parts.join(' / ')}`, 5000); + } + } + } + } + + async loadSettings() { + const data = (await this.loadData()) || {}; + this.settings = { + skillRoot: data.skillRoot || DEFAULT_SETTINGS.skillRoot, + platforms: Array.isArray(data.platforms) && data.platforms.length + ? data.platforms + : DEFAULT_SETTINGS.platforms.map(p => ({ ...p })), + notifyOnStartup: data.notifyOnStartup !== false, + autoScanIntervalMin: typeof data.autoScanIntervalMin === 'number' ? data.autoScanIntervalMin : DEFAULT_SETTINGS.autoScanIntervalMin, + autoInstall: data.autoInstall !== false, + autoImport: data.autoImport === true + }; + } + + async saveSettings() { + await this.saveData(this.settings); + this.startAutoScan(); + this.refreshAllViews(); + } + + async activateView() { + const { workspace } = this.app; + let leaf = workspace.getLeavesOfType(VIEW_TYPE)[0]; + if (!leaf) { + leaf = workspace.getRightLeaf(false); + await leaf.setViewState({ type: VIEW_TYPE, active: true }); + } + workspace.revealLeaf(leaf); + } + + refreshAllViews() { + this.app.workspace.getLeavesOfType(VIEW_TYPE).forEach(l => { + if (l.view && typeof l.view.render === 'function') l.view.render(); + }); + } + + skillRootAbs() { + return path.join(vaultBasePath(this.app), this.settings.skillRoot); + } + + async listSkills() { + const root = this.skillRootAbs(); + if (!(await exists(root))) return []; + let entries; + try { + entries = await fsp.readdir(root, { withFileTypes: true }); + } catch { + return []; + } + const skills = []; + for (const e of entries) { + if (!e.isDirectory()) continue; + if (e.name.startsWith('.')) continue; + const dir = path.join(root, e.name); + const skillMd = path.join(dir, 'SKILL.md'); + let description = ''; + if (await exists(skillMd)) { + try { + const txt = await fsp.readFile(skillMd, 'utf8'); + const m = txt.match(/^---\n([\s\S]*?)\n---/); + if (m) { + const d = m[1].match(/^description:\s*(.+)$/m); + if (d) description = d[1].trim(); + } + } catch {} + } + skills.push({ name: e.name, abs: dir, description }); + } + skills.sort((a, b) => a.name.localeCompare(b.name)); + return skills; + } + + async getStatus(skill, platform) { + const target = path.join(expandHome(platform.target), skill.name); + if (!(await exists(target))) return { state: 'missing', target }; + const link = await readlinkSafe(target); + if (link == null) return { state: 'conflict', target }; + const linkAbs = path.isAbsolute(link) ? link : path.resolve(path.dirname(target), link); + if (path.resolve(linkAbs) === path.resolve(skill.abs)) return { state: 'linked', target }; + return { state: 'mislinked', target, current: linkAbs }; + } + + async install(skill, platform) { + const targetDir = expandHome(platform.target); + try { + await fsp.mkdir(targetDir, { recursive: true }); + } catch (e) { + new obsidian.Notice(`无法创建 Coding 工具的 Skill 目录 ${targetDir}: ${e.message}`); + return false; + } + const target = path.join(targetDir, skill.name); + if (await exists(target)) { + const link = await readlinkSafe(target); + if (link == null) { + new obsidian.Notice(`冲突:${target} 已存在且不是链接,未操作`); + return false; + } + await fsp.unlink(target); + } + try { + await fsp.symlink(skill.abs, target, 'dir'); + } catch (e) { + new obsidian.Notice(`创建链接失败:${e.message}`); + return false; + } + new obsidian.Notice(`✓ 已安装 ${skill.name} → ${platform.name}`); + return true; + } + + async uninstall(skill, platform) { + const target = path.join(expandHome(platform.target), skill.name); + if (!(await exists(target))) return false; + if (!(await isSymlink(target))) { + new obsidian.Notice(`安全保护:这是 Coding 工具自己的 Skill(不是从 Obsidian 链接过去的),插件不敢删`); + return false; + } + try { + await fsp.unlink(target); + } catch (e) { + new obsidian.Notice(`移除失败:${e.message}`); + return false; + } + new obsidian.Notice(`✓ 已移除 ${skill.name} ← ${platform.name}`); + return true; + } + + async listImportable() { + const seen = new Map(); + const vaultSkills = new Set((await this.listSkills()).map(s => s.name)); + for (const p of this.settings.platforms) { + if (!p.enabled) continue; + const dir = expandHome(p.target); + if (!(await exists(dir))) continue; + let entries; + try { entries = await fsp.readdir(dir, { withFileTypes: true }); } + catch { continue; } + for (const e of entries) { + if (!e.isDirectory()) continue; + if (e.name.startsWith('.')) continue; + const full = path.join(dir, e.name); + const st = await lstatSafe(full); + if (!st || st.isSymbolicLink()) continue; + if (vaultSkills.has(e.name)) continue; + if (seen.has(e.name)) continue; + seen.set(e.name, { name: e.name, abs: full, sourcePlatform: p.name }); + } + } + return Array.from(seen.values()).sort((a, b) => a.name.localeCompare(b.name)); + } + + async importSkill(item) { + const root = this.skillRootAbs(); + await fsp.mkdir(root, { recursive: true }); + const dest = path.join(root, item.name); + if (await exists(dest)) { + new obsidian.Notice(`Obsidian 里已存在 ${item.name},跳过`); + return false; + } + try { + await fsp.rename(item.abs, dest); + } catch (e) { + new obsidian.Notice(`移动失败 ${item.name}: ${e.message}`); + return false; + } + const skill = { name: item.name, abs: dest, description: '' }; + for (const p of this.settings.platforms) { + if (!p.enabled) continue; + const target = path.join(expandHome(p.target), item.name); + if (await exists(target)) { + const link = await readlinkSafe(target); + if (link != null) { + try { await fsp.unlink(target); } catch {} + } else { + continue; + } + } + try { await fsp.symlink(skill.abs, target, 'dir'); } catch {} + } + return true; + } + + async installAll() { + const skills = await this.listSkills(); + if (skills.length === 0) { + new obsidian.Notice('未发现任何 skill'); + return; + } + let added = 0, skipped = 0, conflicts = 0; + for (const s of skills) { + for (const p of this.settings.platforms) { + if (!p.enabled) continue; + const st = await this.getStatus(s, p); + if (st.state === 'linked') { skipped++; continue; } + if (st.state === 'conflict') { conflicts++; continue; } + if (await this.install(s, p)) added++; + } + } + new obsidian.Notice(`批量完成:新增 ${added},已存在 ${skipped},冲突 ${conflicts}`); + this.refreshAllViews(); + } + + async listDangling() { + const dangling = []; + for (const p of this.settings.platforms) { + if (!p.enabled) continue; + const dir = expandHome(p.target); + if (!(await exists(dir))) continue; + let entries; + try { entries = await fsp.readdir(dir, { withFileTypes: true }); } + catch { continue; } + for (const e of entries) { + const full = path.join(dir, e.name); + const st = await lstatSafe(full); + if (!st || !st.isSymbolicLink()) continue; + try { + await fsp.stat(full); // 跟随 symlink,失败说明 dangling + } catch { + const link = await readlinkSafe(full); + dangling.push({ platform: p.name, name: e.name, target: full, points: link }); + } + } + } + return dangling; + } + + async cleanupDangling() { + const list = await this.listDangling(); + let count = 0; + for (const d of list) { + try { await fsp.unlink(d.target); count++; } catch {} + } + new obsidian.Notice(`已清理 ${count} 个失效链接`); + this.refreshAllViews(); + return count; + } + + async repairAllMislinked() { + const skills = await this.listSkills(); + let count = 0; + for (const s of skills) { + for (const p of this.settings.platforms) { + if (!p.enabled) continue; + const st = await this.getStatus(s, p); + if (st.state === 'mislinked') { + if (await this.install(s, p)) count++; + } + } + } + new obsidian.Notice(`已修复 ${count} 个指向错误的链接`); + this.refreshAllViews(); + return count; + } + + async getSyncSummary() { + const skills = await this.listSkills(); + const importable = await this.listImportable(); + const dangling = await this.listDangling(); + const enabledPlatforms = this.settings.platforms.filter(p => p.enabled); + let fullySynced = 0, partiallySynced = 0, notInstalled = 0; + let mislinkedSkills = 0, conflictSkills = 0; + for (const s of skills) { + let linkedN = 0, mislinkedN = 0, conflictN = 0; + for (const p of enabledPlatforms) { + const st = await this.getStatus(s, p); + if (st.state === 'linked') linkedN++; + else if (st.state === 'mislinked') mislinkedN++; + else if (st.state === 'conflict') conflictN++; + } + if (mislinkedN > 0) mislinkedSkills++; + if (conflictN > 0) conflictSkills++; + if (enabledPlatforms.length > 0 && linkedN === enabledPlatforms.length) fullySynced++; + else if (linkedN > 0) partiallySynced++; + else notInstalled++; + } + return { + vaultSkillCount: skills.length, + platformCount: enabledPlatforms.length, + fullySynced, partiallySynced, notInstalled, + mislinkedSkills, conflictSkills, + importableCount: importable.length, + danglingCount: dangling.length, + hasIssue: notInstalled > 0 || partiallySynced > 0 || mislinkedSkills > 0 || conflictSkills > 0 || importable.length > 0 || dangling.length > 0 + }; + } + + async checkAndNotifyOnStartup() { + if (!this.settings.notifyOnStartup) return; + const s = await this.getSyncSummary(); + if (!s.hasIssue) return; + const parts = []; + if (s.notInstalled > 0) parts.push(`${s.notInstalled} 未安装`); + if (s.partiallySynced > 0) parts.push(`${s.partiallySynced} 部分同步`); + if (s.importableCount > 0) parts.push(`${s.importableCount} 待导入`); + if (s.mislinkedSkills > 0) parts.push(`${s.mislinkedSkills} 指向错误`); + if (s.danglingCount > 0) parts.push(`${s.danglingCount} 失效`); + if (s.conflictSkills > 0) parts.push(`${s.conflictSkills} 冲突`); + new obsidian.Notice(`Skill Sync: ${parts.join(' / ')}(点击查看)`, 8000); + } +} + +class SkillSyncView extends obsidian.ItemView { + constructor(leaf, plugin) { + super(leaf); + this.plugin = plugin; + this.filterText = ''; + } + getViewType() { return VIEW_TYPE; } + getDisplayText() { return 'Skill Sync'; } + getIcon() { return 'plug'; } + async onOpen() { await this.render(); } + async onClose() {} + + async renderStatusCard(c) { + const card = c.createDiv({ cls: 'css-status-card' }); + const loading = card.createDiv({ cls: 'css-status-loading', text: '检查同步状态...' }); + const s = await this.plugin.getSyncSummary(); + loading.remove(); + + const head = card.createDiv({ cls: 'css-status-head' }); + head.createEl('span', { cls: 'css-status-title', text: '同步状态' }); + head.createEl('span', { + cls: 'css-status-overall ' + (s.hasIssue ? 'has-issue' : 'all-good'), + text: s.hasIssue ? '需要操作' : '全部同步' + }); + + const grid = card.createDiv({ cls: 'css-status-grid' }); + const items = [ + { + label: `Obsidian Skill (×${s.platformCount} 工具)`, value: s.vaultSkillCount, kind: 'info', + tip: `Obsidian 里收藏的 Skill 总数。这些 Skill 会同步到 ${s.platformCount} 个 Coding 工具。` + }, + { + label: '已同步', value: s.fullySynced, kind: s.fullySynced > 0 ? 'good' : 'mute', + tip: '所有启用的 Coding 工具都能用的 Skill 数。' + }, + { + label: '部分同步', value: s.partiallySynced, kind: s.partiallySynced > 0 ? 'warn' : 'mute', action: 'install', + tip: '只在某些 Coding 工具能用、其他工具还没装的 Skill 数。点击一键补齐。' + }, + { + label: '未安装', value: s.notInstalled, kind: s.notInstalled > 0 ? 'warn' : 'mute', action: 'install', + tip: '还没装到任何 Coding 工具的 Skill 数。常见于刚加了新 Skill 或换了新电脑。点击一键安装。' + }, + { + label: '待导入', value: s.importableCount, kind: s.importableCount > 0 ? 'warn' : 'mute', action: 'import', + tip: 'Coding 工具那边自带的、还没收进 Obsidian 管理的 Skill 数。点击挑选哪些要纳入。' + }, + { + label: '指向错误', value: s.mislinkedSkills, kind: s.mislinkedSkills > 0 ? 'warn' : 'mute', action: 'repair', + tip: '链接到了错误位置的 Skill 数(比如换电脑后 Obsidian 路径变了)。点击一键修正。' + }, + { + label: '失效 link', value: s.danglingCount, kind: s.danglingCount > 0 ? 'warn' : 'mute', action: 'cleanup', + tip: '你已经删了源 Skill,但 Coding 工具那边还残留着空链接。点击清理。' + }, + { + label: '冲突', value: s.conflictSkills, kind: s.conflictSkills > 0 ? 'bad' : 'mute', + tip: 'Coding 工具那边已经有同名 Skill(不是从 Obsidian 来的),插件不敢覆盖,需要你手动处理。' + } + ]; + items.forEach(it => { + const cell = grid.createDiv({ cls: `css-status-cell css-${it.kind}` }); + cell.setAttr('aria-label', it.tip); + cell.title = it.tip; + cell.createDiv({ cls: 'css-status-num', text: String(it.value) }); + cell.createDiv({ cls: 'css-status-label', text: it.label }); + if (it.action && it.value > 0) { + cell.addClass('css-clickable'); + cell.onclick = async () => { + if (it.action === 'install') { await this.plugin.installAll(); } + else if (it.action === 'import') { new ImportModal(this.app, this.plugin).open(); return; } + else if (it.action === 'repair') { await this.plugin.repairAllMislinked(); } + else if (it.action === 'cleanup') { await this.plugin.cleanupDangling(); } + await this.render(); + }; + } + }); + } + + async render() { + const c = this.containerEl.children[1]; + c.empty(); + c.addClass('css-skill-sync'); + + const header = c.createDiv({ cls: 'css-header' }); + header.createEl('h3', { text: 'Claude Skill Sync' }); + const btnRow = c.createDiv({ cls: 'css-btn-row' }); + const refreshBtn = btnRow.createEl('button', { text: '刷新' }); + refreshBtn.onclick = () => this.render(); + const installAllBtn = btnRow.createEl('button', { text: '全部安装', cls: 'mod-cta' }); + installAllBtn.onclick = async () => { await this.plugin.installAll(); await this.render(); }; + const importBtn = btnRow.createEl('button', { text: '导入现有' }); + importBtn.onclick = () => new ImportModal(this.app, this.plugin).open(); + + const root = this.plugin.skillRootAbs(); + c.createDiv({ cls: 'css-meta', text: `Skill 文件夹:${root}` }); + + await this.renderStatusCard(c); + + const skills = await this.plugin.listSkills(); + if (skills.length === 0) { + const empty = c.createDiv({ cls: 'css-empty' }); + empty.createEl('div', { text: `还没有任何 Skill。` }); + empty.createEl('div', { text: `在 Obsidian 里创建:${this.plugin.settings.skillRoot}//SKILL.md` }); + return; + } + + const searchWrap = c.createDiv({ cls: 'css-search-wrap' }); + const searchInput = searchWrap.createEl('input', { + type: 'search', + cls: 'css-search-input', + attr: { placeholder: '搜索 Skill 名或描述...' } + }); + searchInput.value = this.filterText || ''; + const countEl = searchWrap.createEl('span', { cls: 'css-search-count' }); + + const listEl = c.createDiv({ cls: 'css-skill-list' }); + + for (const s of skills) { + const card = listEl.createDiv({ cls: 'css-skill' }); + card.dataset.name = s.name.toLowerCase(); + card.dataset.desc = (s.description || '').toLowerCase(); + + const nameRow = card.createDiv({ cls: 'css-name-row' }); + const toggle = nameRow.createSpan({ cls: 'css-toggle', text: s.description ? '▸' : ' ' }); + nameRow.createSpan({ cls: 'css-name', text: s.name }); + + let descEl = null; + if (s.description) { + descEl = card.createEl('div', { cls: 'css-desc', text: s.description }); + descEl.style.display = 'none'; + const toggleDesc = (e) => { + if (e) e.stopPropagation(); + const open = descEl.style.display === 'none'; + descEl.style.display = open ? '' : 'none'; + toggle.setText(open ? '▾' : '▸'); + }; + toggle.style.cursor = 'pointer'; + toggle.onclick = toggleDesc; + nameRow.style.cursor = 'pointer'; + nameRow.onclick = toggleDesc; + } + + for (const p of this.plugin.settings.platforms) { + if (!p.enabled) continue; + const row = card.createDiv({ cls: 'css-row' }); + row.createSpan({ cls: 'css-plat', text: p.name }); + + const status = await this.plugin.getStatus(s, p); + const labels = { linked: '已安装', missing: '未安装', conflict: '冲突', mislinked: '指向其他' }; + row.createSpan({ cls: `css-badge css-${status.state}`, text: labels[status.state] || status.state }); + + if (status.state === 'linked') { + const btn = row.createEl('button', { text: '移除' }); + btn.onclick = async () => { await this.plugin.uninstall(s, p); await this.render(); }; + } else if (status.state === 'missing') { + const btn = row.createEl('button', { text: '安装', cls: 'mod-cta' }); + btn.onclick = async () => { await this.plugin.install(s, p); await this.render(); }; + } else if (status.state === 'mislinked') { + row.createSpan({ cls: 'css-warn', text: `→ ${status.current}` }); + const btn = row.createEl('button', { text: '改指向' }); + btn.onclick = async () => { await this.plugin.install(s, p); await this.render(); }; + } else if (status.state === 'conflict') { + row.createSpan({ cls: 'css-warn', text: 'Coding 工具里已有同名 Skill(不是链接),请手动处理' }); + } + } + } + + const filter = (q) => { + this.filterText = q; + const ql = (q || '').toLowerCase().trim(); + let visible = 0; + listEl.querySelectorAll('.css-skill').forEach(card => { + const match = !ql || card.dataset.name.includes(ql) || card.dataset.desc.includes(ql); + card.style.display = match ? '' : 'none'; + if (match) visible++; + }); + countEl.setText(ql ? `${visible} / ${skills.length}` : `${skills.length}`); + }; + searchInput.oninput = (e) => filter(e.target.value); + filter(this.filterText); + } +} + +class SkillSyncSettingTab extends obsidian.PluginSettingTab { + constructor(app, plugin) { + super(app, plugin); + this.plugin = plugin; + } + + display() { + const { containerEl } = this; + containerEl.empty(); + containerEl.createEl('h2', { text: 'Claude Skill Sync' }); + + new obsidian.Setting(containerEl) + .setName('启动时提醒同步状态') + .setDesc('打开 Obsidian 后扫描一次,如发现待安装/待导入/失效等问题弹通知') + .addToggle(tg => tg + .setValue(this.plugin.settings.notifyOnStartup) + .onChange(async v => { + this.plugin.settings.notifyOnStartup = v; + await this.plugin.saveSettings(); + })); + + containerEl.createEl('h3', { text: '自动同步' }); + + new obsidian.Setting(containerEl) + .setName('定时扫描间隔(分钟)') + .setDesc('后台每隔多少分钟自动检查一次。0 = 关闭定时扫描。建议 5-30。') + .addText(t => t + .setPlaceholder('5') + .setValue(String(this.plugin.settings.autoScanIntervalMin)) + .onChange(async v => { + const n = parseInt(v, 10); + this.plugin.settings.autoScanIntervalMin = isNaN(n) ? 0 : Math.max(0, n); + await this.plugin.saveSettings(); + })); + + new obsidian.Setting(containerEl) + .setName('自动安装新 Skill 到 Coding 工具') + .setDesc('扫描时如发现 Obsidian 有新 Skill 还没装到 Coding 工具,自动建链接。安全(链接可逆)。建议开启。') + .addToggle(tg => tg + .setValue(this.plugin.settings.autoInstall) + .onChange(async v => { + this.plugin.settings.autoInstall = v; + await this.plugin.saveSettings(); + })); + + const importSetting = new obsidian.Setting(containerEl) + .setName('自动收 Coding 工具里的新 Skill 到 Obsidian') + .setDesc('⚠️ 危险:扫描时如发现 Coding 工具里有新的实体 Skill 文件夹,自动移到 Obsidian。移动不可逆。仅当你确定 Coding 工具的 Skill 目录不会被其他工具临时使用时再开。') + .addToggle(tg => tg + .setValue(this.plugin.settings.autoImport) + .onChange(async v => { + this.plugin.settings.autoImport = v; + await this.plugin.saveSettings(); + })); + importSetting.descEl.style.color = 'var(--text-warning, #eab308)'; + + new obsidian.Setting(containerEl) + .setName('Obsidian 中的 Skill 文件夹') + .setDesc('相对 Obsidian 根的路径。下面每个子文件夹视为一个 Skill。') + .addText(t => t + .setPlaceholder('Skills') + .setValue(this.plugin.settings.skillRoot) + .onChange(async v => { + this.plugin.settings.skillRoot = (v || '').trim() || 'Skills'; + await this.plugin.saveSettings(); + })); + + containerEl.createEl('h3', { text: 'Coding 工具' }); + containerEl.createDiv({ cls: 'setting-item-description', text: '安装 = 让 Coding 工具能读到 Obsidian 里的 Skill。路径里 ~ 会自动展开成你的 home 目录。' }); + + this.plugin.settings.platforms.forEach((p, idx) => { + const wrap = containerEl.createDiv({ cls: 'css-platform-edit' }); + new obsidian.Setting(wrap) + .setName(p.name || '(未命名)') + .addText(t => t + .setPlaceholder('名称') + .setValue(p.name) + .onChange(async v => { + this.plugin.settings.platforms[idx].name = v; + await this.plugin.saveSettings(); + })) + .addText(t => t + .setPlaceholder('~/.claude/skills') + .setValue(p.target) + .onChange(async v => { + this.plugin.settings.platforms[idx].target = v.trim(); + await this.plugin.saveSettings(); + })) + .addToggle(tg => tg + .setValue(p.enabled) + .setTooltip('启用') + .onChange(async v => { + this.plugin.settings.platforms[idx].enabled = v; + await this.plugin.saveSettings(); + })) + .addExtraButton(b => b + .setIcon('trash') + .setTooltip('删除该平台') + .onClick(async () => { + this.plugin.settings.platforms.splice(idx, 1); + await this.plugin.saveSettings(); + this.display(); + })); + }); + + new obsidian.Setting(containerEl) + .addButton(b => b + .setButtonText('+ 添加 Coding 工具') + .setCta() + .onClick(() => { + new AddPlatformModal(this.app, this.plugin, () => this.display()).open(); + })); + + containerEl.createEl('h3', { text: '说明' }); + const ul = containerEl.createEl('ul'); + [ + '只在桌面端工作(移动端不支持链接)', + '安装 = 在 Coding 工具的 Skill 目录里建一个链接,指回 Obsidian 里的源 Skill', + '移除只断开链接,不会删除 Obsidian 里的原 Skill(安全保护)', + '若 Coding 工具里已经有同名 Skill(不是从 Obsidian 链接过来的),状态会显示为「冲突」,需手动迁移或重命名', + '跨电脑同步交给 Obsidian 自身(Obsidian Sync / iCloud / Git / Remotely Save)' + ].forEach(t => ul.createEl('li', { text: t })); + } +} + +class ImportModal extends obsidian.Modal { + constructor(app, plugin) { + super(app); + this.plugin = plugin; + this.selected = new Set(); + this.items = []; + } + + async onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass('css-skill-sync-modal'); + contentEl.createEl('h2', { text: '导入 Coding 工具里已有的 Skill 到 Obsidian' }); + contentEl.createEl('p', { + cls: 'css-import-hint', + text: '把 Coding 工具里已有的 Skill 收进 Obsidian 统一管理,之后所有 Coding 工具都能用同一份。已经链接好的或 Obsidian 里已有同名的会被自动过滤。' + }); + + const loading = contentEl.createDiv({ text: '扫描中...' }); + this.items = await this.plugin.listImportable(); + loading.remove(); + + if (this.items.length === 0) { + contentEl.createDiv({ cls: 'css-empty', text: '没有可导入的 Skill' }); + return; + } + + const ctrl = contentEl.createDiv({ cls: 'css-import-ctrl' }); + const selAll = ctrl.createEl('button', { text: '全选' }); + const selNone = ctrl.createEl('button', { text: '清空' }); + const goBtn = ctrl.createEl('button', { text: '导入选中', cls: 'mod-cta' }); + ctrl.createSpan({ cls: 'css-import-count', text: `共 ${this.items.length} 个候选` }); + + const listEl = contentEl.createDiv({ cls: 'css-import-list' }); + const checkboxes = []; + this.items.forEach(item => { + const row = listEl.createDiv({ cls: 'css-import-row' }); + const cb = row.createEl('input', { type: 'checkbox' }); + cb.dataset.name = item.name; + cb.onchange = () => { + if (cb.checked) this.selected.add(item.name); + else this.selected.delete(item.name); + }; + checkboxes.push(cb); + const label = row.createEl('label'); + label.createEl('strong', { text: item.name }); + label.createEl('span', { cls: 'css-source', text: ` ← ${item.sourcePlatform}` }); + label.createEl('div', { cls: 'css-source-path', text: item.abs }); + + const delBtn = row.createEl('button', { cls: 'css-del-btn', text: '删除' }); + delBtn.title = '删除 Coding 工具里这份实体 Skill 文件夹(不可恢复)'; + let confirmTimer = null; + delBtn.onclick = async (e) => { + e.stopPropagation(); + if (delBtn.dataset.confirm === '1') { + if (confirmTimer) clearTimeout(confirmTimer); + try { + await fsp.rm(item.abs, { recursive: true, force: true }); + new obsidian.Notice(`已删除 ${item.name}`); + this.selected.delete(item.name); + row.remove(); + this.items = this.items.filter(x => x.name !== item.name); + ctrl.querySelector('.css-import-count').setText(`共 ${this.items.length} 个候选`); + if (this.items.length === 0) { + listEl.remove(); + contentEl.createDiv({ cls: 'css-empty', text: '没有可导入的 Skill' }); + } + this.plugin.refreshAllViews(); + } catch (err) { + new obsidian.Notice(`删除失败:${err.message}`); + } + return; + } + delBtn.dataset.confirm = '1'; + delBtn.setText('再点确认'); + delBtn.addClass('mod-warning'); + confirmTimer = setTimeout(() => { + delBtn.dataset.confirm = '0'; + delBtn.setText('删除'); + delBtn.removeClass('mod-warning'); + }, 3000); + }; + }); + + selAll.onclick = () => { + checkboxes.forEach(cb => { + cb.checked = true; + this.selected.add(cb.dataset.name); + }); + }; + selNone.onclick = () => { + checkboxes.forEach(cb => { cb.checked = false; }); + this.selected.clear(); + }; + goBtn.onclick = async () => { + if (this.selected.size === 0) { + new obsidian.Notice('未选中任何 Skill'); + return; + } + goBtn.disabled = true; + goBtn.setText('导入中...'); + let count = 0; + for (const it of this.items) { + if (!this.selected.has(it.name)) continue; + if (await this.plugin.importSkill(it)) count++; + } + new obsidian.Notice(`已导入 ${count} 个 Skill 到 Obsidian`); + this.close(); + this.plugin.refreshAllViews(); + }; + } + + onClose() { this.contentEl.empty(); } +} + +class AddPlatformModal extends obsidian.Modal { + constructor(app, plugin, onComplete) { + super(app); + this.plugin = plugin; + this.onComplete = onComplete; + this.selected = new Set(); + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass('css-skill-sync-modal'); + contentEl.addClass('css-add-platform-modal'); + contentEl.createEl('h2', { text: '添加 Coding 工具' }); + + const searchWrap = contentEl.createDiv({ cls: 'css-search-wrap' }); + const searchInput = searchWrap.createEl('input', { + type: 'search', + cls: 'css-search-input', + attr: { placeholder: '搜索预设(如 cursor、gemini)...' } + }); + const countEl = searchWrap.createEl('span', { cls: 'css-search-count' }); + + const existingTargets = new Set( + this.plugin.settings.platforms.map(p => (p.target || '').replace(/\/$/, '')) + ); + const existingNames = new Set(this.plugin.settings.platforms.map(p => p.name)); + + const listEl = contentEl.createDiv({ cls: 'css-preset-list' }); + PRESET_PLATFORMS.forEach(preset => { + const exists = existingNames.has(preset.name) || existingTargets.has(preset.target); + const row = listEl.createDiv({ cls: 'css-preset-row' }); + row.dataset.search = (preset.name + ' ' + preset.target).toLowerCase(); + const cb = row.createEl('input', { type: 'checkbox' }); + if (exists) { + cb.disabled = true; + cb.checked = true; + row.addClass('css-preset-exists'); + } + cb.onchange = () => { + if (cb.checked) this.selected.add(preset.name); + else this.selected.delete(preset.name); + }; + const label = row.createEl('label'); + label.createSpan({ cls: 'css-preset-name', text: preset.name }); + label.createSpan({ cls: 'css-preset-path', text: preset.target }); + if (exists) label.createSpan({ cls: 'css-preset-tag', text: '已添加' }); + }); + + const filter = (q) => { + const ql = (q || '').toLowerCase().trim(); + let visible = 0; + listEl.querySelectorAll('.css-preset-row').forEach(row => { + const match = !ql || row.dataset.search.includes(ql); + row.style.display = match ? '' : 'none'; + if (match) visible++; + }); + countEl.setText(ql ? `${visible} / ${PRESET_PLATFORMS.length}` : `${PRESET_PLATFORMS.length}`); + }; + searchInput.oninput = (e) => filter(e.target.value); + filter(''); + + const customToggle = contentEl.createEl('button', { + cls: 'css-custom-toggle', + text: '+ 自定义 Coding 工具' + }); + const customWrap = contentEl.createDiv({ cls: 'css-custom-wrap' }); + customWrap.style.display = 'none'; + const customRow = customWrap.createDiv({ cls: 'css-custom-row' }); + const nameInput = customRow.createEl('input', { type: 'text' }); + nameInput.placeholder = '名称(如 My Tool)'; + const pathInput = customRow.createEl('input', { type: 'text' }); + pathInput.placeholder = '~/.your-tool/skills'; + customToggle.onclick = () => { + const open = customWrap.style.display === 'none'; + customWrap.style.display = open ? '' : 'none'; + customToggle.setText(open ? '− 收起自定义' : '+ 自定义 Coding 工具'); + if (open) nameInput.focus(); + }; + + const ctrl = contentEl.createDiv({ cls: 'css-import-ctrl' }); + const goBtn = ctrl.createEl('button', { text: '添加', cls: 'mod-cta' }); + goBtn.onclick = async () => { + let added = 0; + for (const preset of PRESET_PLATFORMS) { + if (!this.selected.has(preset.name)) continue; + if (existingNames.has(preset.name) || existingTargets.has(preset.target)) continue; + this.plugin.settings.platforms.push({ name: preset.name, target: preset.target, enabled: true }); + added++; + } + const cn = (nameInput.value || '').trim(); + const cp = (pathInput.value || '').trim(); + if (cn && cp) { + this.plugin.settings.platforms.push({ name: cn, target: cp, enabled: true }); + added++; + } else if (cn || cp) { + new obsidian.Notice('自定义 Coding 工具需要同时填名称和路径'); + return; + } + if (added === 0) { + new obsidian.Notice('未选中任何预设也未填自定义'); + return; + } + await this.plugin.saveSettings(); + new obsidian.Notice(`已添加 ${added} 个 Coding 工具`); + this.close(); + if (this.onComplete) this.onComplete(); + }; + } + + onClose() { this.contentEl.empty(); } +} + +module.exports = SkillSyncPlugin; diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..469ce99 --- /dev/null +++ b/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "claude-skill-sync", + "name": "Claude Skill Sync", + "version": "0.1.0", + "minAppVersion": "1.4.0", + "description": "Manage AI coding agent Skills (Claude Code, Codex, Cursor, Gemini, and 14+ more) from your Obsidian vault. One source, symlinked to each tool. Cross-machine sync via your existing vault sync.", + "author": "Chocolae", + "authorUrl": "https://github.com/2949984428", + "isDesktopOnly": true +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..f089683 --- /dev/null +++ b/styles.css @@ -0,0 +1,356 @@ +.css-skill-sync { + padding: 8px 10px; +} +.css-skill-sync .css-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 4px; +} +.css-skill-sync .css-header h3 { + margin: 0; + font-size: 14px; +} +.css-skill-sync .css-btn-row { + display: flex; + gap: 6px; + margin-bottom: 8px; +} +.css-skill-sync .css-btn-row button { + flex: 1; + font-size: 12px; +} +.css-skill-sync .css-meta { + font-size: 11px; + color: var(--text-muted); + margin-bottom: 10px; + word-break: break-all; +} +.css-skill-sync .css-status-card { + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + padding: 8px; + margin-bottom: 10px; + background: var(--background-secondary); +} +.css-skill-sync .css-status-loading { + font-size: 11px; + color: var(--text-muted); + text-align: center; + padding: 4px; +} +.css-skill-sync .css-status-head { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; + font-size: 12px; +} +.css-skill-sync .css-status-title { font-weight: 600; } +.css-skill-sync .css-status-overall { + font-size: 10px; + padding: 2px 6px; + border-radius: 3px; +} +.css-skill-sync .css-status-overall.all-good { + background: rgba(16, 185, 129, 0.15); + color: #10b981; +} +.css-skill-sync .css-status-overall.has-issue { + background: rgba(234, 179, 8, 0.15); + color: #eab308; +} +.css-skill-sync .css-status-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; +} +.css-skill-sync .css-status-cell { + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + padding: 6px 8px; + text-align: center; +} +.css-skill-sync .css-status-cell.css-clickable { + cursor: pointer; + transition: background 0.1s; +} +.css-skill-sync .css-status-cell.css-clickable:hover { + background: var(--background-modifier-hover); +} +.css-skill-sync .css-status-num { + font-size: 16px; + font-weight: 600; + line-height: 1.2; +} +.css-skill-sync .css-status-label { + font-size: 10px; + color: var(--text-muted); + margin-top: 2px; +} +.css-skill-sync .css-status-cell.css-good .css-status-num { color: #10b981; } +.css-skill-sync .css-status-cell.css-warn .css-status-num { color: #eab308; } +.css-skill-sync .css-status-cell.css-bad .css-status-num { color: #ef4444; } +.css-skill-sync .css-status-cell.css-info .css-status-num { color: var(--text-normal); } +.css-skill-sync .css-status-cell.css-mute .css-status-num { color: var(--text-muted); } +.css-skill-sync .css-empty { + font-size: 12px; + color: var(--text-muted); + padding: 12px; + border: 1px dashed var(--background-modifier-border); + border-radius: 6px; +} +.css-skill-sync .css-search-wrap { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 8px; +} +.css-skill-sync .css-search-input { + flex: 1; + font-size: 12px; + padding: 4px 8px; + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + background: var(--background-primary); +} +.css-skill-sync .css-search-count { + font-size: 10px; + color: var(--text-muted); + white-space: nowrap; + min-width: 30px; + text-align: right; +} +.css-skill-sync .css-skill-list { + display: flex; + flex-direction: column; +} +.css-skill-sync .css-skill { + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + padding: 8px 10px; + margin-bottom: 6px; + background: var(--background-secondary); +} +.css-skill-sync .css-name-row { + display: flex; + align-items: center; + gap: 4px; + user-select: none; +} +.css-skill-sync .css-toggle { + font-size: 10px; + color: var(--text-muted); + width: 12px; + display: inline-block; + text-align: center; +} +.css-skill-sync .css-name { + font-weight: 600; + font-size: 13px; +} +.css-skill-sync .css-desc { + font-size: 11px; + color: var(--text-muted); + margin: 6px 0 4px 16px; + line-height: 1.4; + padding-left: 6px; + border-left: 2px solid var(--background-modifier-border); +} +.css-skill-sync .css-row { + display: flex; + align-items: center; + gap: 6px; + margin-top: 4px; + font-size: 11px; +} +.css-skill-sync .css-plat { + flex: 1; + color: var(--text-muted); +} +.css-skill-sync .css-badge { + padding: 1px 6px; + border-radius: 3px; + font-size: 10px; + font-weight: 500; +} +.css-skill-sync .css-linked { background: rgba(16, 185, 129, 0.15); color: #10b981; } +.css-skill-sync .css-missing { background: rgba(255, 255, 255, 0.06); color: var(--text-muted); } +.css-skill-sync .css-conflict { background: rgba(239, 68, 68, 0.15); color: #ef4444; } +.css-skill-sync .css-mislinked { background: rgba(234, 179, 8, 0.15); color: #eab308; } +.css-skill-sync .css-warn { + font-size: 10px; + color: var(--text-warning, #eab308); + margin-left: 4px; +} +.css-skill-sync .css-row button { + font-size: 10px; + padding: 2px 8px; + height: auto; +} +.css-platform-edit { + border-left: 2px solid var(--background-modifier-border); + padding-left: 8px; + margin-bottom: 4px; +} +.css-skill-sync-modal .css-import-hint { + font-size: 12px; + color: var(--text-muted); + margin: 4px 0 12px; + line-height: 1.5; +} +.css-skill-sync-modal .css-import-ctrl { + display: flex; + gap: 6px; + align-items: center; + margin-bottom: 8px; +} +.css-skill-sync-modal .css-import-count { + margin-left: auto; + font-size: 11px; + color: var(--text-muted); +} +.css-skill-sync-modal .css-import-list { + max-height: 50vh; + overflow-y: auto; + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + padding: 4px; +} +.css-skill-sync-modal .css-import-row { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 6px 8px; + border-radius: 4px; +} +.css-skill-sync-modal .css-import-row:hover { + background: var(--background-modifier-hover); +} +.css-skill-sync-modal .css-import-row label { + flex: 1; + cursor: pointer; + font-size: 12px; +} +.css-skill-sync-modal .css-del-btn { + font-size: 11px; + padding: 2px 8px; + background: transparent; + border: 1px solid var(--background-modifier-border); + color: var(--text-muted); + border-radius: 3px; + cursor: pointer; + white-space: nowrap; +} +.css-skill-sync-modal .css-del-btn:hover { + border-color: #ef4444; + color: #ef4444; +} +.css-skill-sync-modal .css-del-btn.mod-warning { + background: #ef4444; + border-color: #ef4444; + color: #fff; +} +.css-skill-sync-modal .css-source { + color: var(--text-muted); + font-size: 11px; +} +.css-skill-sync-modal .css-source-path { + font-size: 10px; + color: var(--text-faint); + word-break: break-all; + margin-top: 2px; +} +.css-skill-sync-modal .css-empty { + color: var(--text-muted); + padding: 16px; + text-align: center; +} +.css-skill-sync-modal .css-custom-row { + display: flex; + gap: 6px; + margin-bottom: 10px; +} +.css-skill-sync-modal .css-custom-row input { + flex: 1; + font-size: 12px; + padding: 4px 8px; + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + background: var(--background-primary); +} +.css-skill-sync-modal .css-import-row input[type="checkbox"]:disabled + label { + opacity: 0.5; +} + +/* —— 添加平台 modal —— */ +.css-add-platform-modal .css-search-wrap { + margin-bottom: 8px; +} +.css-add-platform-modal .css-preset-list { + max-height: 320px; + overflow-y: auto; + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + margin-bottom: 10px; +} +.css-add-platform-modal .css-preset-row { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 8px; + font-size: 12px; + border-bottom: 1px solid var(--background-modifier-border); +} +.css-add-platform-modal .css-preset-row:last-child { + border-bottom: none; +} +.css-add-platform-modal .css-preset-row:hover { + background: var(--background-modifier-hover); +} +.css-add-platform-modal .css-preset-row label { + flex: 1; + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + margin: 0; +} +.css-add-platform-modal .css-preset-name { + font-weight: 500; + min-width: 110px; +} +.css-add-platform-modal .css-preset-path { + color: var(--text-muted); + font-size: 11px; + font-family: var(--font-monospace); + flex: 1; +} +.css-add-platform-modal .css-preset-tag { + font-size: 10px; + color: var(--text-muted); + padding: 1px 6px; + background: var(--background-modifier-border); + border-radius: 3px; +} +.css-add-platform-modal .css-preset-exists { + opacity: 0.5; +} +.css-add-platform-modal .css-custom-toggle { + font-size: 12px; + background: transparent; + border: 1px dashed var(--background-modifier-border); + color: var(--text-muted); + padding: 4px 10px; + border-radius: 4px; + cursor: pointer; + width: 100%; + margin-bottom: 8px; +} +.css-add-platform-modal .css-custom-toggle:hover { + background: var(--background-modifier-hover); + color: var(--text-normal); +} +.css-add-platform-modal .css-custom-wrap { + margin-bottom: 10px; +} diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..cdffaed --- /dev/null +++ b/versions.json @@ -0,0 +1,3 @@ +{ + "0.1.0": "1.4.0" +}