dsh-ccswitch-sync 0.1.0 → 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/README.md +1 -1
- package/README.zh.md +1 -1
- 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
|
@@ -131,7 +131,7 @@ on the next run.
|
|
|
131
131
|
## Development
|
|
132
132
|
|
|
133
133
|
```sh
|
|
134
|
-
|
|
134
|
+
npm test # splicing (idempotency, markers, BOM) + file-level sync
|
|
135
135
|
node scripts/check-manifest.mjs # dependency-free manifest shape check
|
|
136
136
|
```
|
|
137
137
|
|
package/README.zh.md
CHANGED
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.1",
|
|
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.1",
|
|
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": {
|