dsh-ccswitch-sync 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -2
- package/README.zh.md +144 -0
- package/cordis.patch.yml +5 -0
- package/dsh-plugin.json +52 -0
- package/lib/index.js +185 -0
- package/lib/sync.mjs +215 -0
- package/package.json +50 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 msyzzm
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,150 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-ccswitch-sync
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[中文](README.zh.md) · [npm](https://www.npmjs.com/package/dsh-ccswitch-sync) · [dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI) · [Plugin directory](https://dshtui.com/plugins)
|
|
4
|
+
|
|
5
|
+
A [dsh-TUI](https://dshtui.com) plugin that mirrors the DSH configuration **CC Switch** keeps
|
|
6
|
+
in its own profile into the `dsh-tui` profile, and shows how fresh the mirror is in the footer.
|
|
7
|
+
|
|
8
|
+
CC Switch writes `llm-pi-ai.providers` and `cc-switch-mcp-*` rows into
|
|
9
|
+
`~/.dsh/profiles/web/cordis.patch.yml` (falling back to `desktop`). dsh-TUI runs its own
|
|
10
|
+
profile, `~/.dsh/profiles/dsh-tui`, so those providers and MCP servers never show up there.
|
|
11
|
+
This plugin ports the CC Switch mirror into the plugin itself: the same text splice, executed
|
|
12
|
+
from inside the TUI, with a footer field so you can see whether the mirror is current.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
ccs 3p 4mcp 1m ← 3 providers, 4 MCP servers, patch written 1 minute ago
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
# from npm
|
|
22
|
+
dsh plugin --profile dsh-tui add dsh-ccswitch-sync
|
|
23
|
+
|
|
24
|
+
# or from a local checkout
|
|
25
|
+
dsh plugin --profile dsh-tui add link:/path/to/dsh-ccswitch-sync
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Inside the terminal, `/plugin install <spec>` works too. Restart dsh-TUI after installing —
|
|
29
|
+
a bundle is only composed when the process starts.
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
dsh plugin --profile dsh-tui remove dsh-ccswitch-sync
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Use
|
|
36
|
+
|
|
37
|
+
| | |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `/ccswitch` | Provider count, MCP count, patch age |
|
|
40
|
+
| `/ccswitch sync` | Mirror now (this rewrites the profile patch, see below) |
|
|
41
|
+
| footer field `ccswitch` | Same counters, refreshed every 60s; `left-tail`/`right-head`/`right-tail` via the `slot` option |
|
|
42
|
+
|
|
43
|
+
The footer field is a plugin field, so `/settings → statusBar.pluginFields` can hide it and
|
|
44
|
+
`statusBar.pluginFieldsOrder` can place it per slot group. Turn the field off and the command
|
|
45
|
+
still works.
|
|
46
|
+
|
|
47
|
+
## When it syncs
|
|
48
|
+
|
|
49
|
+
**It never syncs on its own.** Installing, activating, reloading and switching sessions all
|
|
50
|
+
leave the profile untouched:
|
|
51
|
+
|
|
52
|
+
- `/ccswitch sync` — mirror once, now.
|
|
53
|
+
- `DSH_CCSWITCH_SYNC_ON_START=1` — opt in to mirroring once at activation.
|
|
54
|
+
- No file watcher, no timer that writes. The only timer refreshes the footer text.
|
|
55
|
+
|
|
56
|
+
That default is deliberate. dsh-TUI profiles use `patchReload: live`, so writing
|
|
57
|
+
`cordis.patch.yml` reloads the whole composition — and the row that runs this plugin lives in
|
|
58
|
+
that same file. A mirror that fires on activation would reload the profile that just activated
|
|
59
|
+
it. Doing it on demand keeps that reload a deliberate act.
|
|
60
|
+
|
|
61
|
+
## How the mirror works
|
|
62
|
+
|
|
63
|
+
The managed rows go into one marked block at the end of the target patch:
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
# >>> cc-switch sync (managed) >>>
|
|
67
|
+
- id: llm-pi-ai
|
|
68
|
+
config:
|
|
69
|
+
providers: { ... }
|
|
70
|
+
- insert:
|
|
71
|
+
- id: cc-switch-mcp-context7
|
|
72
|
+
...
|
|
73
|
+
# <<< cc-switch sync (managed) <<<
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- **Text splice, not YAML round-trip.** Blocks are copied verbatim, so comments, ordering and
|
|
77
|
+
quoting in the source survive. No YAML dependency, no re-serialisation surprises.
|
|
78
|
+
- **Idempotent.** The composed text is compared with the current file; identical bytes mean no
|
|
79
|
+
write, no `mtime` change, no reload. This is what keeps the live reload from looping.
|
|
80
|
+
- **Atomic.** Written to `<patch>.candidate.yml` and renamed over the target.
|
|
81
|
+
- **Backed up.** The previous patch is copied to `~/.dsh-tui/ccswitch-sync/backups/` before
|
|
82
|
+
each rewrite; the newest 20 are kept.
|
|
83
|
+
- **Inside the markers only.** A stale `llm-pi-ai` row outside the block is removed (otherwise
|
|
84
|
+
the composition would have two) — nothing else outside the block is touched.
|
|
85
|
+
- **Source preference.** `web` is mirrored when it exists, `desktop` otherwise.
|
|
86
|
+
|
|
87
|
+
## What it does not touch
|
|
88
|
+
|
|
89
|
+
- `~/.dsh/.credentials.yaml` and every credential. Provider entries reference
|
|
90
|
+
`CC_SWITCH_DSH_*` environment names; the secrets stay where CC Switch put them.
|
|
91
|
+
- `~/.dsh/skills` and any shared skill tree.
|
|
92
|
+
- Rows outside the managed block, and every other profile.
|
|
93
|
+
- CC Switch's own files: it only reads them.
|
|
94
|
+
|
|
95
|
+
## Declared permissions
|
|
96
|
+
|
|
97
|
+
`dsh-plugin.json` declares exactly three (`/plugin` shows the same rows):
|
|
98
|
+
|
|
99
|
+
| Permission | Scope |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `dsh-ccswitch-sync.profile.read` | `~/.dsh/profiles/{web,desktop}/cordis.patch.yml` |
|
|
102
|
+
| `dsh-ccswitch-sync.profile.write` | `~/.dsh/profiles/dsh-tui/cordis.patch.yml` |
|
|
103
|
+
| `dsh-ccswitch-sync.state.write` | `~/.dsh-tui/ccswitch-sync/` |
|
|
104
|
+
|
|
105
|
+
Permissions are behavioural declarations, not an OS sandbox: as with any dsh-TUI plugin, this
|
|
106
|
+
code runs in-process with the privileges of your terminal (`trusted-in-process`).
|
|
107
|
+
|
|
108
|
+
## Uninstall and leftover data
|
|
109
|
+
|
|
110
|
+
`disable` marks the row `disabled`; `remove` takes the bundle out of the profile. Both are
|
|
111
|
+
reversible/ordinary profile operations. **Neither deletes user data**, so after a `remove`:
|
|
112
|
+
|
|
113
|
+
| Left behind | Where |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| Patch backups and `plugin.log` | `~/.dsh-tui/ccswitch-sync/` |
|
|
116
|
+
| The managed block in the profile patch | `~/.dsh/profiles/dsh-tui/cordis.patch.yml` |
|
|
117
|
+
|
|
118
|
+
Delete that directory if you want the backups gone. To drop the managed block, delete the two
|
|
119
|
+
`# >>> / # <<<` marker lines and everything between them — `/ccswitch sync` recreates the block
|
|
120
|
+
on the next run.
|
|
121
|
+
|
|
122
|
+
## Compatibility
|
|
123
|
+
|
|
124
|
+
- dsh-TUI ≥ 0.20.0 (the two host seams it uses: footer fields `tuiStatus.setField`, and the
|
|
125
|
+
`commands` service). Both are probed softly through `ctx.inject`; if either is missing the
|
|
126
|
+
plugin degrades to a no-op instead of breaking startup.
|
|
127
|
+
- Manifest: Community v0.15 (`dsh-plugin.json`), facet `v1alpha1`.
|
|
128
|
+
- Node `^22.19 || >=24`, ESM only, zero runtime dependencies.
|
|
129
|
+
- Windows, macOS and Linux. All paths come from `DSH_HOME`/`~`, never from a hardcoded drive.
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
npm test # splicing (idempotency, markers, BOM) + file-level sync
|
|
135
|
+
node scripts/check-manifest.mjs # dependency-free manifest shape check
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The authoritative manifest validator is the Community v0.15 parser (`@dsh-std/manifest`);
|
|
139
|
+
`dsh-plugin.json` here parses and projects cleanly under `0.1.1-rc.2`.
|
|
140
|
+
|
|
141
|
+
## Links
|
|
142
|
+
|
|
143
|
+
- dsh-TUI: <https://github.com/ccch1mneyyy/dsh-TUI> · <https://dshtui.com>
|
|
144
|
+
- Plugin admission and development guide:
|
|
145
|
+
<https://github.com/ccch1mneyyy/dsh-TUI/blob/main/tui-profile/docs/plugin-admission-and-development.md>
|
|
146
|
+
- Ecosystem organisation: <https://github.com/dsh-tui-ecosystem>
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# dsh-ccswitch-sync
|
|
2
|
+
|
|
3
|
+
[English](README.md) · [npm](https://www.npmjs.com/package/dsh-ccswitch-sync) · [dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI) · [插件目录](https://dshtui.com/plugins)
|
|
4
|
+
|
|
5
|
+
把 **CC Switch** 写在自己 profile 里的 DSH 配置,镜像进 `dsh-tui` profile 的
|
|
6
|
+
[dsh-TUI](https://dshtui.com) 插件;底栏顺带显示这份镜像有多新。
|
|
7
|
+
|
|
8
|
+
CC Switch 把 `llm-pi-ai.providers` 和 `cc-switch-mcp-*` 写进
|
|
9
|
+
`~/.dsh/profiles/web/cordis.patch.yml`(没有就退到 `desktop`)。dsh-TUI 跑的是自己的
|
|
10
|
+
profile `~/.dsh/profiles/dsh-tui`,所以那些 provider 与 MCP 在 TUI 里一个都看不见。
|
|
11
|
+
本插件把 CC Switch 的镜像逻辑搬进了插件自己:同一套文本拼接,在 TUI 进程内执行,
|
|
12
|
+
再用一个底栏字段告诉你镜像是不是新鲜的。
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
ccs 3p 4mcp 1m ← 3 个 provider、4 个 MCP、1 分钟前写入
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
# 从 npm 装
|
|
22
|
+
dsh plugin --profile dsh-tui add dsh-ccswitch-sync
|
|
23
|
+
|
|
24
|
+
# 或从本地检出装
|
|
25
|
+
dsh plugin --profile dsh-tui add link:/path/to/dsh-ccswitch-sync
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
终端里 `/plugin install <spec>` 同样可用。装完**重启 dsh-TUI**——bundle 只在新进程启动时
|
|
29
|
+
组合。
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
dsh plugin --profile dsh-tui remove dsh-ccswitch-sync
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 用法
|
|
36
|
+
|
|
37
|
+
| | |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `/ccswitch` | provider 数、MCP 数、patch 年龄 |
|
|
40
|
+
| `/ccswitch sync` | 立刻镜像一次(会重写 profile 补丁,见下) |
|
|
41
|
+
| 底栏字段 `ccswitch` | 同样的计数,每 60 秒刷新;`slot` 选项可放 `left-tail` / `right-head` / `right-tail` |
|
|
42
|
+
|
|
43
|
+
底栏字段属于插件字段:`/settings → statusBar.pluginFields` 能关掉它,
|
|
44
|
+
`statusBar.pluginFieldsOrder` 能按槽组给它排序。关掉字段不影响命令。
|
|
45
|
+
|
|
46
|
+
## 什么时候同步
|
|
47
|
+
|
|
48
|
+
**它不会自己同步。** 安装、激活、重载、切会话都不会动 profile:
|
|
49
|
+
|
|
50
|
+
- `/ccswitch sync` —— 现在镜像一次。
|
|
51
|
+
- `DSH_CCSWITCH_SYNC_ON_START=1` —— 显式选择「激活时镜像一次」。
|
|
52
|
+
- 没有文件监听,没有会写盘的定时器。唯一的定时器只刷新底栏文本。
|
|
53
|
+
|
|
54
|
+
这个默认值是刻意的。dsh-TUI 的 profile 是 `patchReload: live`:写
|
|
55
|
+
`cordis.patch.yml` 会重载整份组合——而跑这个插件的那一行就写在同一份文件里。
|
|
56
|
+
激活即镜像等于让插件重载刚刚激活它的 profile。改成手动,那次重载才是一个明确的动作。
|
|
57
|
+
|
|
58
|
+
## 镜像怎么做的
|
|
59
|
+
|
|
60
|
+
托管的行会进目标补丁末尾的一个标记区块:
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
# >>> cc-switch sync (managed) >>>
|
|
64
|
+
- id: llm-pi-ai
|
|
65
|
+
config:
|
|
66
|
+
providers: { ... }
|
|
67
|
+
- insert:
|
|
68
|
+
- id: cc-switch-mcp-context7
|
|
69
|
+
...
|
|
70
|
+
# <<< cc-switch sync (managed) <<<
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- **文本拼接,不是 YAML 往返。** 区块逐字节照搬,注释、顺序、引号风格都不会被改写。
|
|
74
|
+
没有 YAML 依赖,也就没有重新序列化带来的意外。
|
|
75
|
+
- **幂等。** 拼装结果与当前文件逐字节比较,一样就一个字节都不写:`mtime` 不变,
|
|
76
|
+
不触发重载。这是让 live reload 不打转的关键。
|
|
77
|
+
- **原子写。** 先写 `<patch>.candidate.yml`,再 rename 覆盖目标。
|
|
78
|
+
- **留备份。** 每次重写前把旧补丁拷到 `~/.dsh-tui/ccswitch-sync/backups/`,保留最新 20 份。
|
|
79
|
+
- **只动标记以内。** 区块外若残留一条 `llm-pi-ai`,会被去掉(否则组合里会有两条);
|
|
80
|
+
区块之外的其他行一律不碰。
|
|
81
|
+
- **来源优先。** 有 `web` 就镜像 `web`,否则用 `desktop`。
|
|
82
|
+
|
|
83
|
+
## 不碰什么
|
|
84
|
+
|
|
85
|
+
- `~/.dsh/.credentials.yaml` 与任何凭证。provider 条目引用的是 `CC_SWITCH_DSH_*`
|
|
86
|
+
环境变量名,密钥留在 CC Switch 放的位置。
|
|
87
|
+
- `~/.dsh/skills` 与任何共享技能树。
|
|
88
|
+
- 托管区块以外的行,以及其他所有 profile。
|
|
89
|
+
- CC Switch 自己的文件:只读,不写。
|
|
90
|
+
|
|
91
|
+
## 声明的权限
|
|
92
|
+
|
|
93
|
+
`dsh-plugin.json` 里正好三条(`/plugin` 的授权矩阵显示同样的行):
|
|
94
|
+
|
|
95
|
+
| 权限 | 范围 |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| `dsh-ccswitch-sync.profile.read` | `~/.dsh/profiles/{web,desktop}/cordis.patch.yml` |
|
|
98
|
+
| `dsh-ccswitch-sync.profile.write` | `~/.dsh/profiles/dsh-tui/cordis.patch.yml` |
|
|
99
|
+
| `dsh-ccswitch-sync.state.write` | `~/.dsh-tui/ccswitch-sync/` |
|
|
100
|
+
|
|
101
|
+
权限是行为声明,不是 OS 沙箱:与任何 dsh-TUI 插件一样,这段代码以进程内全信任
|
|
102
|
+
(`trusted-in-process`)运行,拥有你终端的权限。
|
|
103
|
+
|
|
104
|
+
## 卸载与残留数据
|
|
105
|
+
|
|
106
|
+
`disable` 把行标成 `disabled`;`remove` 把 bundle 从 profile 里摘掉。两者都是普通的、
|
|
107
|
+
可复原的 profile 操作。**它们都不删用户数据**,所以 `remove` 之后还会留下:
|
|
108
|
+
|
|
109
|
+
| 残留 | 位置 |
|
|
110
|
+
| --- | --- |
|
|
111
|
+
| 补丁备份与 `plugin.log` | `~/.dsh-tui/ccswitch-sync/` |
|
|
112
|
+
| profile 补丁里的托管区块 | `~/.dsh/profiles/dsh-tui/cordis.patch.yml` |
|
|
113
|
+
|
|
114
|
+
要把备份删干净,删那个目录即可。要去掉托管区块,删掉两行 `# >>>` / `# <<<` 标记
|
|
115
|
+
以及它们之间的内容——下次 `/ccswitch sync` 会重新造出来。
|
|
116
|
+
|
|
117
|
+
## 兼容性
|
|
118
|
+
|
|
119
|
+
- dsh-TUI ≥ 0.20.0(用到两个宿主接缝:底栏字段 `tuiStatus.setField` 与 `commands` 服务)。
|
|
120
|
+
两者都经 `ctx.inject` 软探测;缺了其中一个时插件退化为空操作,不会拖垮启动。
|
|
121
|
+
- 清单:Community v0.15(`dsh-plugin.json`),facet `v1alpha1`。
|
|
122
|
+
- Node `^22.19 || >=24`,纯 ESM,零运行时依赖。
|
|
123
|
+
- Windows / macOS / Linux。路径全部来自 `DSH_HOME` 与 `~`,没有写死的盘符。
|
|
124
|
+
|
|
125
|
+
## 开发
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
npm test # 拼接逻辑(幂等、标记、BOM)+ 文件级同步
|
|
129
|
+
node scripts/check-manifest.mjs # 无依赖的清单形状检查
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
权威校验器是 Community v0.15 parser(`@dsh-std/manifest`);本仓库的 `dsh-plugin.json`
|
|
133
|
+
在 `0.1.1-rc.2` 下 parse 与 project 均通过。
|
|
134
|
+
|
|
135
|
+
## 链接
|
|
136
|
+
|
|
137
|
+
- dsh-TUI:<https://github.com/ccch1mneyyy/dsh-TUI> · <https://dshtui.com>
|
|
138
|
+
- 插件准入与开发指南:
|
|
139
|
+
<https://github.com/ccch1mneyyy/dsh-TUI/blob/main/tui-profile/docs/plugin-admission-and-development.md>
|
|
140
|
+
- 生态组织:<https://github.com/dsh-tui-ecosystem>
|
|
141
|
+
|
|
142
|
+
## 许可
|
|
143
|
+
|
|
144
|
+
MIT。
|
package/cordis.patch.yml
ADDED
package/dsh-plugin.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/@dsh-std/manifest@0.1.1-rc.2/schema/dsh-plugin-0.15.schema.json",
|
|
3
|
+
"id": "msyzzm.dsh-ccswitch-sync",
|
|
4
|
+
"name": "CC Switch → dsh-TUI sync",
|
|
5
|
+
"version": "0.1.1",
|
|
6
|
+
"manifestVersion": "0.15",
|
|
7
|
+
"facets": {
|
|
8
|
+
"host": {
|
|
9
|
+
"entry": "lib/index.js",
|
|
10
|
+
"apiVersion": "v1alpha1"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"requires": {
|
|
14
|
+
"contracts": []
|
|
15
|
+
},
|
|
16
|
+
"permissions": [
|
|
17
|
+
{
|
|
18
|
+
"name": "dsh-ccswitch-sync.profile.read",
|
|
19
|
+
"scope": "~/.dsh/profiles/web/cordis.patch.yml, ~/.dsh/profiles/desktop/cordis.patch.yml",
|
|
20
|
+
"reason": "Reads the CC Switch-managed parts (llm-pi-ai providers, cc-switch-mcp-* rows) out of the source profile patches. Read-only; nothing else in those files is inspected."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "dsh-ccswitch-sync.profile.write",
|
|
24
|
+
"scope": "~/.dsh/profiles/dsh-tui/cordis.patch.yml",
|
|
25
|
+
"reason": "Writes the mirrored rows into one marked block of the dsh-tui profile patch. The write is atomic, only happens when the composed text actually differs, and never touches text outside the block markers."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "dsh-ccswitch-sync.state.write",
|
|
29
|
+
"scope": "~/.dsh-tui/ccswitch-sync/",
|
|
30
|
+
"reason": "Keeps the last 20 copies of the target patch before each rewrite, plus a diagnostic log. Best effort: a failure here never blocks or rewrites a sync."
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"contributes": {
|
|
34
|
+
"commands": [
|
|
35
|
+
{
|
|
36
|
+
"id": "msyzzm.ccswitch",
|
|
37
|
+
"title": "ccswitch",
|
|
38
|
+
"description": "Show the mirror status (providers, MCP servers, patch age); `/ccswitch sync` mirrors now."
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"subscriptions": [],
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"source": {
|
|
45
|
+
"repository": "https://github.com/msyzzm/dsh-ccswitch-sync"
|
|
46
|
+
},
|
|
47
|
+
"compat": {
|
|
48
|
+
"hosts": [
|
|
49
|
+
"dsh-tui >=0.20.0"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-ccswitch-sync — mirror CC Switch's DSH configuration into the dsh-tui
|
|
3
|
+
* profile, and show how fresh it is.
|
|
4
|
+
*
|
|
5
|
+
* The mirror runs on `/ccswitch sync`, or at activation when
|
|
6
|
+
* `DSH_CCSWITCH_SYNC_ON_START=1` is set. Writing `cordis.patch.yml` from a row
|
|
7
|
+
* inside that same file triggers the profile's live reload, which is harmless
|
|
8
|
+
* once at startup and disruptive in the middle of a turn — hence off by
|
|
9
|
+
* default. `syncOnce` only writes when the composed text actually differs, so
|
|
10
|
+
* the reload it causes settles after one pass.
|
|
11
|
+
*
|
|
12
|
+
* Service discovery goes through `ctx.inject`: on a cold boot the status and
|
|
13
|
+
* command services mount after this row, so a one-shot `ctx.get` at apply time
|
|
14
|
+
* would silently miss them.
|
|
15
|
+
*/
|
|
16
|
+
import { appendFile, mkdir, readFile, stat } from 'node:fs/promises'
|
|
17
|
+
import { appendFileSync } from 'node:fs'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import { STATE_DIR, defaultPaths, syncOnce } from './sync.mjs'
|
|
20
|
+
|
|
21
|
+
export const name = 'ccswitch-sync'
|
|
22
|
+
|
|
23
|
+
const LOG_PATH = join(STATE_DIR, 'plugin.log')
|
|
24
|
+
|
|
25
|
+
// Import-time probe: distinguishes "module never imported" from "apply never
|
|
26
|
+
// called" when the row fails to mount on a cold boot. Opt-in — a plugin should
|
|
27
|
+
// not create files just by being imported.
|
|
28
|
+
const DEBUG = process.env.DSH_CCSWITCH_DEBUG === '1'
|
|
29
|
+
if (DEBUG) {
|
|
30
|
+
try {
|
|
31
|
+
appendFileSync(LOG_PATH, `[${new Date().toISOString()}] import: module evaluated pid=${process.pid}\n`)
|
|
32
|
+
} catch {
|
|
33
|
+
// probe only
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Same file the sync writes, so the status always describes what was synced. */
|
|
37
|
+
const PATCH_PATH = defaultPaths().target
|
|
38
|
+
|
|
39
|
+
const FIELD_KEY = 'ccswitch'
|
|
40
|
+
const REFRESH_MS = 60_000
|
|
41
|
+
const MAX_STATUS_CELLS = 20
|
|
42
|
+
const STARTUP_DELAY_MS = 200
|
|
43
|
+
/** Activation-time sync is opt-in; installing must never rewrite the profile. */
|
|
44
|
+
const SYNC_ON_START = process.env.DSH_CCSWITCH_SYNC_ON_START === '1'
|
|
45
|
+
|
|
46
|
+
async function log(line) {
|
|
47
|
+
try {
|
|
48
|
+
await mkdir(STATE_DIR, { recursive: true })
|
|
49
|
+
await appendFile(LOG_PATH, `[${new Date().toISOString()}] ${line}\n`)
|
|
50
|
+
} catch {
|
|
51
|
+
// logging is best effort
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function humanAge(ms) {
|
|
56
|
+
if (!Number.isFinite(ms) || ms < 0) return '?'
|
|
57
|
+
const seconds = Math.floor(ms / 1000)
|
|
58
|
+
if (seconds < 90) return `${seconds}s`
|
|
59
|
+
const minutes = Math.floor(seconds / 60)
|
|
60
|
+
if (minutes < 90) return `${minutes}m`
|
|
61
|
+
const hours = Math.floor(minutes / 60)
|
|
62
|
+
if (hours < 48) return `${hours}h`
|
|
63
|
+
return `${Math.floor(hours / 24)}d`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function snapshot() {
|
|
67
|
+
const text = await readFile(PATCH_PATH, 'utf8').catch(() => '')
|
|
68
|
+
const statInfo = await stat(PATCH_PATH).catch(() => undefined)
|
|
69
|
+
return {
|
|
70
|
+
providers: (text.match(/^\s+apiKeyEnv:/gm) ?? []).length,
|
|
71
|
+
mcp: (text.match(/id: cc-switch-mcp-/g) ?? []).length,
|
|
72
|
+
patchedAt: statInfo ? statInfo.mtimeMs : undefined,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function statusText(snap) {
|
|
77
|
+
if (snap.patchedAt === undefined) return 'ccs no patch'
|
|
78
|
+
const age = humanAge(Date.now() - snap.patchedAt)
|
|
79
|
+
// The key is internal to the host, so the field needs its own visible label.
|
|
80
|
+
return `ccs ${snap.providers}p ${snap.mcp}mcp ${age}`.slice(0, MAX_STATUS_CELLS)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function describe(snap) {
|
|
84
|
+
return [
|
|
85
|
+
`providers: ${snap.providers}`,
|
|
86
|
+
`mcp: ${snap.mcp}`,
|
|
87
|
+
snap.patchedAt === undefined
|
|
88
|
+
? 'patch: 未找到'
|
|
89
|
+
: `patch: ${new Date(snap.patchedAt).toLocaleString()}(${humanAge(Date.now() - snap.patchedAt)} 前)`,
|
|
90
|
+
'',
|
|
91
|
+
'/ccswitch sync 立即同步(会触发 profile 重载)',
|
|
92
|
+
].join('\n')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function apply(ctx) {
|
|
96
|
+
let syncing
|
|
97
|
+
let refreshField
|
|
98
|
+
void log('apply: mounted')
|
|
99
|
+
|
|
100
|
+
const runSync = (origin) => {
|
|
101
|
+
if (syncing !== undefined) return syncing
|
|
102
|
+
syncing = (async () => {
|
|
103
|
+
try {
|
|
104
|
+
const result = await syncOnce(defaultPaths())
|
|
105
|
+
await log(`sync(${origin}): ${result.note}`)
|
|
106
|
+
await refreshField?.()
|
|
107
|
+
return result
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const message = error && error.message ? error.message : String(error)
|
|
110
|
+
await log(`sync(${origin}) failed: ${message}`)
|
|
111
|
+
return { changed: false, note: `failed: ${message}` }
|
|
112
|
+
} finally {
|
|
113
|
+
syncing = undefined
|
|
114
|
+
}
|
|
115
|
+
})()
|
|
116
|
+
return syncing
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
ctx.inject(['tuiStatus'], (child) => {
|
|
120
|
+
let disposer
|
|
121
|
+
const refresh = async () => {
|
|
122
|
+
try {
|
|
123
|
+
const snap = await snapshot()
|
|
124
|
+
disposer = child.tuiStatus.setField(FIELD_KEY, statusText(snap), {
|
|
125
|
+
slot: 'right-head',
|
|
126
|
+
tone: 'dim',
|
|
127
|
+
maxCells: MAX_STATUS_CELLS,
|
|
128
|
+
detail:
|
|
129
|
+
snap.patchedAt === undefined
|
|
130
|
+
? 'cc-switch → dsh-tui: profile patch not found'
|
|
131
|
+
: `cc-switch → dsh-tui: ${snap.providers} providers, ${snap.mcp} MCP`,
|
|
132
|
+
})
|
|
133
|
+
} catch {
|
|
134
|
+
// the status bar is optional
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
void refresh()
|
|
138
|
+
refreshField = refresh
|
|
139
|
+
const timer = setInterval(() => void refresh(), REFRESH_MS)
|
|
140
|
+
ctx.effect(() => () => {
|
|
141
|
+
clearInterval(timer)
|
|
142
|
+
try {
|
|
143
|
+
disposer?.()
|
|
144
|
+
} catch {
|
|
145
|
+
// ignore
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
ctx.inject(['commands'], (child) => {
|
|
151
|
+
try {
|
|
152
|
+
const dispose = child.commands.register({
|
|
153
|
+
name: 'ccswitch',
|
|
154
|
+
description: 'CC Switch → dsh-tui 同步状态(/ccswitch sync 立即同步)',
|
|
155
|
+
handler: async (invocation) => {
|
|
156
|
+
const raw = String(invocation?.rawInput ?? '').trim().toLowerCase()
|
|
157
|
+
if (raw === 'sync') {
|
|
158
|
+
const result = await runSync('manual')
|
|
159
|
+
return {
|
|
160
|
+
kind: 'success',
|
|
161
|
+
text: result.changed
|
|
162
|
+
? `已同步并写入 profile:${result.note}\n(profile 会重载,稍等几秒)`
|
|
163
|
+
: `无需写入:${result.note}`,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { kind: 'success', text: describe(await snapshot()) }
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
ctx.effect(() => () => {
|
|
170
|
+
try {
|
|
171
|
+
dispose?.()
|
|
172
|
+
} catch {
|
|
173
|
+
// ignore
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
} catch {
|
|
177
|
+
// commands service may be absent in bare compositions
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
if (SYNC_ON_START) {
|
|
182
|
+
const starting = setTimeout(() => void runSync('startup'), STARTUP_DELAY_MS)
|
|
183
|
+
ctx.effect(() => () => clearTimeout(starting))
|
|
184
|
+
}
|
|
185
|
+
}
|
package/lib/sync.mjs
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mirror itself: copy the CC Switch-managed parts of a DSH profile patch
|
|
3
|
+
* (`llm-pi-ai.providers` and the `cc-switch-mcp-*` insert rows) into the
|
|
4
|
+
* dsh-tui profile patch, inside a marked block.
|
|
5
|
+
*
|
|
6
|
+
* Pure text splicing on purpose: the source blocks are copied verbatim, so no
|
|
7
|
+
* YAML parser is needed and nothing outside the markers is touched. The write
|
|
8
|
+
* is atomic and only happens when the composed text actually differs, which is
|
|
9
|
+
* what keeps the live reload from looping.
|
|
10
|
+
*/
|
|
11
|
+
import { copyFile, mkdir, readdir, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises'
|
|
12
|
+
import { homedir } from 'node:os'
|
|
13
|
+
import { dirname, join } from 'node:path'
|
|
14
|
+
|
|
15
|
+
export const BEGIN_MARK = '# >>> cc-switch sync (managed) >>>'
|
|
16
|
+
export const END_MARK = '# <<< cc-switch sync (managed) <<<'
|
|
17
|
+
|
|
18
|
+
const EMPTY_PROVIDERS_BLOCK = '- id: llm-pi-ai\n config:\n providers: {}\n'
|
|
19
|
+
const KEEP_BACKUPS = 20
|
|
20
|
+
const STABLE_READ_DELAY_MS = 150
|
|
21
|
+
const STABLE_READ_ATTEMPTS = 5
|
|
22
|
+
/** Plugin state (backups, log). The env override keeps tests off the real directory. */
|
|
23
|
+
export const STATE_DIR = join(process.env.CCSWITCH_SYNC_STATE || join(homedir(), '.dsh-tui'), 'ccswitch-sync')
|
|
24
|
+
|
|
25
|
+
export function defaultPaths() {
|
|
26
|
+
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
27
|
+
const profiles = join(home, 'profiles')
|
|
28
|
+
const targetDir = process.env.DSH_PROFILE_DIR || join(profiles, 'dsh-tui')
|
|
29
|
+
return {
|
|
30
|
+
sources: [
|
|
31
|
+
join(profiles, 'web', 'cordis.patch.yml'),
|
|
32
|
+
join(profiles, 'desktop', 'cordis.patch.yml'),
|
|
33
|
+
],
|
|
34
|
+
target: join(targetDir, 'cordis.patch.yml'),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A leading BOM would hide the first "- " entry from blockSpans. */
|
|
39
|
+
function stripBom(text) {
|
|
40
|
+
return text.startsWith('\uFEFF') ? text.slice(1) : text
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function linesWithEnds(text) {
|
|
44
|
+
const out = []
|
|
45
|
+
let index = 0
|
|
46
|
+
while (index < text.length) {
|
|
47
|
+
const newline = text.indexOf('\n', index)
|
|
48
|
+
if (newline === -1) {
|
|
49
|
+
out.push(text.slice(index))
|
|
50
|
+
break
|
|
51
|
+
}
|
|
52
|
+
out.push(text.slice(index, newline + 1))
|
|
53
|
+
index = newline + 1
|
|
54
|
+
}
|
|
55
|
+
return out
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Character spans of every top-level "- " entry in a YAML sequence. */
|
|
59
|
+
export function blockSpans(text) {
|
|
60
|
+
const spans = []
|
|
61
|
+
let position = 0
|
|
62
|
+
let start
|
|
63
|
+
for (const line of linesWithEnds(text)) {
|
|
64
|
+
if (line.startsWith('- ')) {
|
|
65
|
+
if (start !== undefined) spans.push({ start, end: position })
|
|
66
|
+
start = position
|
|
67
|
+
}
|
|
68
|
+
position += line.length
|
|
69
|
+
}
|
|
70
|
+
if (start !== undefined) spans.push({ start, end: position })
|
|
71
|
+
return spans
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function firstLine(block) {
|
|
75
|
+
const newline = block.indexOf('\n')
|
|
76
|
+
return newline === -1 ? block : block.slice(0, newline)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isLlmPiAiBlock(block) {
|
|
80
|
+
return /^- id:\s*llm-pi-ai\s*$/.test(firstLine(block).trimEnd())
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function isManagedInsert(block) {
|
|
84
|
+
return block.startsWith('- insert:') && block.includes('cc-switch-mcp-')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The blocks to mirror, always with an `llm-pi-ai` entry. */
|
|
88
|
+
export function extractManaged(rawSourceText) {
|
|
89
|
+
const sourceText = stripBom(rawSourceText)
|
|
90
|
+
const blocks = blockSpans(sourceText).map(({ start, end }) => sourceText.slice(start, end))
|
|
91
|
+
const wanted = blocks.filter((block) => isLlmPiAiBlock(block) || isManagedInsert(block))
|
|
92
|
+
if (!wanted.some(isLlmPiAiBlock)) wanted.unshift(EMPTY_PROVIDERS_BLOCK)
|
|
93
|
+
return wanted.map((block) => (block.endsWith('\n') ? block : `${block}\n`))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function dropBlocks(text, predicate) {
|
|
97
|
+
const spans = blockSpans(text)
|
|
98
|
+
.filter(({ start, end }) => predicate(text.slice(start, end)))
|
|
99
|
+
let result = text
|
|
100
|
+
for (let i = spans.length - 1; i >= 0; i -= 1) {
|
|
101
|
+
result = result.slice(0, spans[i].start) + result.slice(spans[i].end)
|
|
102
|
+
}
|
|
103
|
+
return result
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Remove a lone `[]` line (an empty YAML sequence) when nothing else is left. */
|
|
107
|
+
function dropEmptyFlowSequence(text) {
|
|
108
|
+
const meaningful = text
|
|
109
|
+
.split('\n')
|
|
110
|
+
.map((line) => line.replace(/^\uFEFF/, '').trim())
|
|
111
|
+
.filter((line) => line !== '' && !line.startsWith('#'))
|
|
112
|
+
if (meaningful.length !== 1 || meaningful[0] !== '[]') return text
|
|
113
|
+
return (
|
|
114
|
+
text
|
|
115
|
+
.split('\n')
|
|
116
|
+
.filter((line) => line.replace(/^\uFEFF/, '').trim() !== '[]')
|
|
117
|
+
.join('\n')
|
|
118
|
+
.replace(/\n+$/, '') + '\n'
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Splice the managed block into the target patch text. */
|
|
123
|
+
export function composePatch(rawTargetText, managedBlocks) {
|
|
124
|
+
const targetText = stripBom(rawTargetText)
|
|
125
|
+
const managed = `${BEGIN_MARK}\n${managedBlocks.join('')}${END_MARK}\n`
|
|
126
|
+
let head = targetText
|
|
127
|
+
let tail = ''
|
|
128
|
+
const begin = targetText.indexOf(BEGIN_MARK)
|
|
129
|
+
const end = targetText.indexOf(END_MARK)
|
|
130
|
+
if (begin !== -1 && end > begin) {
|
|
131
|
+
head = targetText.slice(0, begin)
|
|
132
|
+
tail = targetText.slice(end + END_MARK.length)
|
|
133
|
+
}
|
|
134
|
+
head = dropBlocks(head, isLlmPiAiBlock)
|
|
135
|
+
tail = dropBlocks(tail, isLlmPiAiBlock)
|
|
136
|
+
head = dropEmptyFlowSequence(head)
|
|
137
|
+
const trimmedHead = head.replace(/\n+$/, '')
|
|
138
|
+
let out = trimmedHead === '' ? managed : `${trimmedHead}\n${managed}`
|
|
139
|
+
if (tail.trim() !== '') out += tail.replace(/^\n+/, '')
|
|
140
|
+
return out
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Read until two consecutive reads agree, so a file another process is still
|
|
145
|
+
* writing is not mirrored half-written. Gives up after a few attempts and
|
|
146
|
+
* returns the latest read.
|
|
147
|
+
*/
|
|
148
|
+
async function readStable(path) {
|
|
149
|
+
try {
|
|
150
|
+
let previous = await readFile(path, 'utf8')
|
|
151
|
+
for (let attempt = 1; attempt < STABLE_READ_ATTEMPTS; attempt += 1) {
|
|
152
|
+
await new Promise((resolve) => setTimeout(resolve, STABLE_READ_DELAY_MS))
|
|
153
|
+
const current = await readFile(path, 'utf8')
|
|
154
|
+
if (current === previous) return current
|
|
155
|
+
previous = current
|
|
156
|
+
}
|
|
157
|
+
return previous
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error && error.code === 'ENOENT') return undefined
|
|
160
|
+
throw error
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function backupTarget(target) {
|
|
165
|
+
try {
|
|
166
|
+
const stamp = new Date()
|
|
167
|
+
.toISOString()
|
|
168
|
+
.replace(/[-:]/g, '')
|
|
169
|
+
.replace('T', '-')
|
|
170
|
+
.slice(0, 15)
|
|
171
|
+
const dir = join(STATE_DIR, 'backups')
|
|
172
|
+
await mkdir(dir, { recursive: true })
|
|
173
|
+
const dest = join(dir, `cordis.patch.${stamp}.yml`)
|
|
174
|
+
await copyFile(target, dest)
|
|
175
|
+
const names = (await readdir(dir)).filter((name) => name.endsWith('.yml')).sort()
|
|
176
|
+
for (const name of names.slice(0, Math.max(0, names.length - KEEP_BACKUPS))) {
|
|
177
|
+
await unlink(join(dir, name)).catch(() => {})
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// backups are best-effort; never block the write
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Mirror the first existing source (web, then desktop) into the target.
|
|
186
|
+
* @returns `{ changed, note }` — `changed` is true only when bytes were written.
|
|
187
|
+
*/
|
|
188
|
+
export async function syncOnce(paths = defaultPaths()) {
|
|
189
|
+
const candidates = []
|
|
190
|
+
for (const path of paths.sources) {
|
|
191
|
+
try {
|
|
192
|
+
await stat(path)
|
|
193
|
+
candidates.push(path)
|
|
194
|
+
} catch {
|
|
195
|
+
// missing profile: skip
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (candidates.length === 0) return { changed: false, note: 'no source profile patch found' }
|
|
199
|
+
|
|
200
|
+
const source = candidates[0]
|
|
201
|
+
const profile = dirname(source).split(/[\\/]/).pop()
|
|
202
|
+
const sourceText = await readStable(source)
|
|
203
|
+
if (sourceText === undefined) return { changed: false, note: `source=${profile}: unreadable` }
|
|
204
|
+
|
|
205
|
+
let targetText = (await readStable(paths.target)) ?? ''
|
|
206
|
+
const composed = composePatch(targetText, extractManaged(sourceText))
|
|
207
|
+
if (composed === targetText) return { changed: false, note: `source=${profile}: already in sync` }
|
|
208
|
+
|
|
209
|
+
await mkdir(dirname(paths.target), { recursive: true })
|
|
210
|
+
const stage = `${paths.target}.candidate.yml`
|
|
211
|
+
await writeFile(stage, composed, 'utf8')
|
|
212
|
+
await backupTarget(paths.target)
|
|
213
|
+
await rename(stage, paths.target)
|
|
214
|
+
return { changed: true, note: `source=${profile}: wrote ${composed.length} bytes` }
|
|
215
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ccswitch-sync",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Mirror CC Switch's DSH providers and MCP servers into the dsh-tui profile, and show how fresh the mirror is.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh",
|
|
7
|
+
"dsh-tui",
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"cc-switch",
|
|
10
|
+
"ccswitch",
|
|
11
|
+
"cordis"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/msyzzm/dsh-ccswitch-sync#readme",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/msyzzm/dsh-ccswitch-sync.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/msyzzm/dsh-ccswitch-sync/issues"
|
|
20
|
+
},
|
|
21
|
+
"author": "msyzzm",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "lib/index.js",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./lib/index.js",
|
|
27
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"lib",
|
|
32
|
+
"cordis.patch.yml",
|
|
33
|
+
"dsh-plugin.json"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"registry": "https://registry.npmjs.org/"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": "^22.19 || >=24"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"check": "node --check lib/index.js && node --check lib/sync.mjs",
|
|
44
|
+
"check:manifest": "node scripts/check-manifest.mjs",
|
|
45
|
+
"test": "node --test \"test/*.test.mjs\""
|
|
46
|
+
},
|
|
47
|
+
"dsh": {
|
|
48
|
+
"bundle": {
|
|
49
|
+
"patch": "./cordis.patch.yml"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|