dsh-ccswitch-sync 0.1.0 → 0.1.2
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/README.md +9 -2
- package/README.zh.md +9 -2
- package/dsh-plugin.json +1 -1
- package/lib/index.js +3 -7
- package/lib/sync.mjs +30 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
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
4
|
|
|
5
|
+
<p>
|
|
6
|
+
<img src="docs/footer.png" alt="Footer field: ccs 3p 3mcp 33m" width="358"><br>
|
|
7
|
+
<img src="docs/ccswitch.png" alt="/ccswitch output: providers, MCP servers, patch age" width="443">
|
|
8
|
+
</p>
|
|
9
|
+
|
|
5
10
|
A [dsh-TUI](https://dshtui.com) plugin that mirrors the DSH configuration **CC Switch** keeps
|
|
6
11
|
in its own profile into the `dsh-tui` profile, and shows how fresh the mirror is in the footer.
|
|
7
12
|
|
|
@@ -88,7 +93,9 @@ The managed rows go into one marked block at the end of the target patch:
|
|
|
88
93
|
|
|
89
94
|
- `~/.dsh/.credentials.yaml` and every credential. Provider entries reference
|
|
90
95
|
`CC_SWITCH_DSH_*` environment names; the secrets stay where CC Switch put them.
|
|
91
|
-
- `~/.dsh/skills` and any shared skill tree.
|
|
96
|
+
- `~/.dsh/skills` and any shared skill tree — and skills need no mirror. CC Switch links them
|
|
97
|
+
into `~/.dsh/skills`, which every profile reads (dsh-tui included, through `dsh-base`'s
|
|
98
|
+
`skill-filesystem`). Enable a skill for DSH in CC Switch and dsh-TUI picks it up.
|
|
92
99
|
- Rows outside the managed block, and every other profile.
|
|
93
100
|
- CC Switch's own files: it only reads them.
|
|
94
101
|
|
|
@@ -131,7 +138,7 @@ on the next run.
|
|
|
131
138
|
## Development
|
|
132
139
|
|
|
133
140
|
```sh
|
|
134
|
-
|
|
141
|
+
npm test # splicing (idempotency, markers, BOM) + file-level sync
|
|
135
142
|
node scripts/check-manifest.mjs # dependency-free manifest shape check
|
|
136
143
|
```
|
|
137
144
|
|
package/README.zh.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
+
<p>
|
|
6
|
+
<img src="docs/footer.png" alt="底栏字段:ccs 3p 3mcp 33m" width="358"><br>
|
|
7
|
+
<img src="docs/ccswitch.png" alt="/ccswitch 输出:provider 数、MCP 数、patch 时间" width="443">
|
|
8
|
+
</p>
|
|
9
|
+
|
|
5
10
|
把 **CC Switch** 写在自己 profile 里的 DSH 配置,镜像进 `dsh-tui` profile 的
|
|
6
11
|
[dsh-TUI](https://dshtui.com) 插件;底栏顺带显示这份镜像有多新。
|
|
7
12
|
|
|
@@ -84,7 +89,9 @@ dsh plugin --profile dsh-tui remove dsh-ccswitch-sync
|
|
|
84
89
|
|
|
85
90
|
- `~/.dsh/.credentials.yaml` 与任何凭证。provider 条目引用的是 `CC_SWITCH_DSH_*`
|
|
86
91
|
环境变量名,密钥留在 CC Switch 放的位置。
|
|
87
|
-
- `~/.dsh/skills`
|
|
92
|
+
- `~/.dsh/skills` 与任何共享技能树——技能也不需要镜像。CC Switch 把技能链接进
|
|
93
|
+
`~/.dsh/skills`,所有 profile 都读这个目录(dsh-tui 通过 `dsh-base` 的 `skill-filesystem`
|
|
94
|
+
读取)。在 CC Switch 里给 DSH 启用某个技能,dsh-TUI 就能用。
|
|
88
95
|
- 托管区块以外的行,以及其他所有 profile。
|
|
89
96
|
- CC Switch 自己的文件:只读,不写。
|
|
90
97
|
|
|
@@ -125,7 +132,7 @@ dsh plugin --profile dsh-tui remove dsh-ccswitch-sync
|
|
|
125
132
|
## 开发
|
|
126
133
|
|
|
127
134
|
```sh
|
|
128
|
-
|
|
135
|
+
npm test # 拼接逻辑(幂等、标记、BOM)+ 文件级同步
|
|
129
136
|
node scripts/check-manifest.mjs # 无依赖的清单形状检查
|
|
130
137
|
```
|
|
131
138
|
|
package/dsh-plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://unpkg.com/@dsh-std/manifest@0.1.1-rc.2/schema/dsh-plugin-0.15.schema.json",
|
|
3
3
|
"id": "msyzzm.dsh-ccswitch-sync",
|
|
4
4
|
"name": "CC Switch → dsh-TUI sync",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.2",
|
|
6
6
|
"manifestVersion": "0.15",
|
|
7
7
|
"facets": {
|
|
8
8
|
"host": {
|
package/lib/index.js
CHANGED
|
@@ -15,14 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { appendFile, mkdir, readFile, stat } from 'node:fs/promises'
|
|
17
17
|
import { appendFileSync } from 'node:fs'
|
|
18
|
-
import { homedir } from 'node:os'
|
|
19
18
|
import { join } from 'node:path'
|
|
20
|
-
import { defaultPaths, syncOnce } from './sync.mjs'
|
|
19
|
+
import { STATE_DIR, defaultPaths, syncOnce } from './sync.mjs'
|
|
21
20
|
|
|
22
21
|
export const name = 'ccswitch-sync'
|
|
23
22
|
|
|
24
|
-
const HOME = homedir()
|
|
25
|
-
const STATE_DIR = join(HOME, '.dsh-tui', 'ccswitch-sync')
|
|
26
23
|
const LOG_PATH = join(STATE_DIR, 'plugin.log')
|
|
27
24
|
|
|
28
25
|
// Import-time probe: distinguishes "module never imported" from "apply never
|
|
@@ -36,9 +33,8 @@ if (DEBUG) {
|
|
|
36
33
|
// probe only
|
|
37
34
|
}
|
|
38
35
|
}
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
const PATCH_PATH = join(PROFILE_DIR, 'cordis.patch.yml')
|
|
36
|
+
/** Same file the sync writes, so the status always describes what was synced. */
|
|
37
|
+
const PATCH_PATH = defaultPaths().target
|
|
42
38
|
|
|
43
39
|
const FIELD_KEY = 'ccswitch'
|
|
44
40
|
const REFRESH_MS = 60_000
|
package/lib/sync.mjs
CHANGED
|
@@ -17,21 +17,29 @@ export const END_MARK = '# <<< cc-switch sync (managed) <<<'
|
|
|
17
17
|
|
|
18
18
|
const EMPTY_PROVIDERS_BLOCK = '- id: llm-pi-ai\n config:\n providers: {}\n'
|
|
19
19
|
const KEEP_BACKUPS = 20
|
|
20
|
-
|
|
21
|
-
const
|
|
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')
|
|
22
24
|
|
|
23
25
|
export function defaultPaths() {
|
|
24
26
|
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
25
27
|
const profiles = join(home, 'profiles')
|
|
28
|
+
const targetDir = process.env.DSH_PROFILE_DIR || join(profiles, 'dsh-tui')
|
|
26
29
|
return {
|
|
27
30
|
sources: [
|
|
28
31
|
join(profiles, 'web', 'cordis.patch.yml'),
|
|
29
32
|
join(profiles, 'desktop', 'cordis.patch.yml'),
|
|
30
33
|
],
|
|
31
|
-
target: join(
|
|
34
|
+
target: join(targetDir, 'cordis.patch.yml'),
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
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
|
+
|
|
35
43
|
function linesWithEnds(text) {
|
|
36
44
|
const out = []
|
|
37
45
|
let index = 0
|
|
@@ -77,7 +85,8 @@ export function isManagedInsert(block) {
|
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
/** The blocks to mirror, always with an `llm-pi-ai` entry. */
|
|
80
|
-
export function extractManaged(
|
|
88
|
+
export function extractManaged(rawSourceText) {
|
|
89
|
+
const sourceText = stripBom(rawSourceText)
|
|
81
90
|
const blocks = blockSpans(sourceText).map(({ start, end }) => sourceText.slice(start, end))
|
|
82
91
|
const wanted = blocks.filter((block) => isLlmPiAiBlock(block) || isManagedInsert(block))
|
|
83
92
|
if (!wanted.some(isLlmPiAiBlock)) wanted.unshift(EMPTY_PROVIDERS_BLOCK)
|
|
@@ -87,7 +96,6 @@ export function extractManaged(sourceText) {
|
|
|
87
96
|
function dropBlocks(text, predicate) {
|
|
88
97
|
const spans = blockSpans(text)
|
|
89
98
|
.filter(({ start, end }) => predicate(text.slice(start, end)))
|
|
90
|
-
.map(({ start, end }) => ({ start, end }))
|
|
91
99
|
let result = text
|
|
92
100
|
for (let i = spans.length - 1; i >= 0; i -= 1) {
|
|
93
101
|
result = result.slice(0, spans[i].start) + result.slice(spans[i].end)
|
|
@@ -112,7 +120,8 @@ function dropEmptyFlowSequence(text) {
|
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
/** Splice the managed block into the target patch text. */
|
|
115
|
-
export function composePatch(
|
|
123
|
+
export function composePatch(rawTargetText, managedBlocks) {
|
|
124
|
+
const targetText = stripBom(rawTargetText)
|
|
116
125
|
const managed = `${BEGIN_MARK}\n${managedBlocks.join('')}${END_MARK}\n`
|
|
117
126
|
let head = targetText
|
|
118
127
|
let tail = ''
|
|
@@ -131,12 +140,21 @@ export function composePatch(targetText, managedBlocks) {
|
|
|
131
140
|
return out
|
|
132
141
|
}
|
|
133
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
|
+
*/
|
|
134
148
|
async function readStable(path) {
|
|
135
149
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
140
158
|
} catch (error) {
|
|
141
159
|
if (error && error.code === 'ENOENT') return undefined
|
|
142
160
|
throw error
|
|
@@ -150,7 +168,7 @@ async function backupTarget(target) {
|
|
|
150
168
|
.replace(/[-:]/g, '')
|
|
151
169
|
.replace('T', '-')
|
|
152
170
|
.slice(0, 15)
|
|
153
|
-
const dir = join(STATE_DIR, '
|
|
171
|
+
const dir = join(STATE_DIR, 'backups')
|
|
154
172
|
await mkdir(dir, { recursive: true })
|
|
155
173
|
const dest = join(dir, `cordis.patch.${stamp}.yml`)
|
|
156
174
|
await copyFile(target, dest)
|
|
@@ -164,7 +182,7 @@ async function backupTarget(target) {
|
|
|
164
182
|
}
|
|
165
183
|
|
|
166
184
|
/**
|
|
167
|
-
* Mirror the
|
|
185
|
+
* Mirror the first existing source (web, then desktop) into the target.
|
|
168
186
|
* @returns `{ changed, note }` — `changed` is true only when bytes were written.
|
|
169
187
|
*/
|
|
170
188
|
export async function syncOnce(paths = defaultPaths()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ccswitch-sync",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Mirror CC Switch's DSH providers and MCP servers into the dsh-tui profile, and show how fresh the mirror is.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"scripts": {
|
|
43
43
|
"check": "node --check lib/index.js && node --check lib/sync.mjs",
|
|
44
44
|
"check:manifest": "node scripts/check-manifest.mjs",
|
|
45
|
-
"test": "node --test test
|
|
45
|
+
"test": "node --test \"test/*.test.mjs\""
|
|
46
46
|
},
|
|
47
47
|
"dsh": {
|
|
48
48
|
"bundle": {
|