@code_tin/dsh-sync 0.0.0-stage → 0.1.0
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 +87 -2
- package/client.js +458 -0
- package/cordis.patch.yml +8 -0
- package/icon.svg +12 -0
- package/index.js +167 -0
- package/lib/remote.js +62 -0
- package/lib/sync.js +581 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +60 -4
package/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half of `dsh-sync`.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - own the git mirror engine (`lib/sync.js`);
|
|
6
|
+
* - register the human `/sync` command;
|
|
7
|
+
* - expose the same operations to the Settings page through a Remote service.
|
|
8
|
+
*
|
|
9
|
+
* The browser half only renders the settings page; every operation lives here
|
|
10
|
+
* so the command line and the page cannot disagree.
|
|
11
|
+
*/
|
|
12
|
+
import os from 'node:os'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import { SessionGitSync, validateBranch } from './lib/sync.js'
|
|
15
|
+
import { registerRemoteService } from './lib/remote.js'
|
|
16
|
+
|
|
17
|
+
const SERVICE_KEY = 'dshSync'
|
|
18
|
+
const NAMESPACE = 'dsh-sync'
|
|
19
|
+
|
|
20
|
+
/** The command needs the registry; without it the plugin has nothing to do. */
|
|
21
|
+
export const inject = ['commands']
|
|
22
|
+
|
|
23
|
+
function formatTime(value) {
|
|
24
|
+
if (!value) return '无'
|
|
25
|
+
const date = new Date(value)
|
|
26
|
+
if (Number.isNaN(date.getTime())) return value
|
|
27
|
+
return date.toLocaleString('zh-CN', { hour12: false })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatStatus(status) {
|
|
31
|
+
const lines = [
|
|
32
|
+
'DSH 会话 Git 同步',
|
|
33
|
+
`远程仓库: ${status.remote || '(未配置,在 设置 → 插件 → 会话 Git 同步 中填写)'}`,
|
|
34
|
+
`分支: ${status.branch}`,
|
|
35
|
+
`本地会话: ${status.local.sessions} 个会话 / ${status.local.files} 个文件`,
|
|
36
|
+
`镜像仓库: ${status.mirror.initialized
|
|
37
|
+
? `${status.mirror.sessions} 个会话 / ${status.mirror.files} 个文件`
|
|
38
|
+
: '尚未初始化(首次 /sync push 时创建)'}`,
|
|
39
|
+
]
|
|
40
|
+
if (status.state.lastPushAt) {
|
|
41
|
+
lines.push(`上次推送: ${formatTime(status.state.lastPushAt)} ${status.state.lastPushOk === false ? '失败' : '成功'}`)
|
|
42
|
+
}
|
|
43
|
+
if (status.state.lastPullAt) {
|
|
44
|
+
lines.push(`上次拉取: ${formatTime(status.state.lastPullAt)} ${status.state.lastPullOk === false ? '失败' : '成功'}`)
|
|
45
|
+
}
|
|
46
|
+
if (status.state.lastError) lines.push(`最近错误: ${status.state.lastError}`)
|
|
47
|
+
lines.push('用法: /sync | /sync pull | /sync push')
|
|
48
|
+
return lines.join('\n')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function success(text) {
|
|
52
|
+
return { kind: 'success', text }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function failure(text) {
|
|
56
|
+
return { kind: 'error', text }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function apply(ctx) {
|
|
60
|
+
const home = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
|
|
61
|
+
const sync = new SessionGitSync({
|
|
62
|
+
home,
|
|
63
|
+
logger: ctx.logger,
|
|
64
|
+
flush: () => ctx.get('sessionPersistence')?.flush(),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
ctx.effect(() => ctx.commands.register({
|
|
68
|
+
name: 'sync',
|
|
69
|
+
description: '在设备之间通过 Git 同步 DSH 会话:status / pull / push',
|
|
70
|
+
input: { hint: 'status | pull | push' },
|
|
71
|
+
async handler(invocation) {
|
|
72
|
+
const argument = String(invocation.rawInput ?? '').trim().toLowerCase()
|
|
73
|
+
try {
|
|
74
|
+
if (argument === '' || argument === 'status') {
|
|
75
|
+
return success(formatStatus(await sync.status()))
|
|
76
|
+
}
|
|
77
|
+
if (argument === 'help') {
|
|
78
|
+
return success([
|
|
79
|
+
'/sync 查看同步状态',
|
|
80
|
+
'/sync pull 拉取远程会话到本地(不覆盖本地已有会话)',
|
|
81
|
+
'/sync push 把本地会话推送到 Git 远程仓库',
|
|
82
|
+
'/sync test 测试已保存的 Git 地址',
|
|
83
|
+
].join('\n'))
|
|
84
|
+
}
|
|
85
|
+
if (argument === 'pull') {
|
|
86
|
+
const result = await sync.pull()
|
|
87
|
+
if (result.ok === false) return failure(`拉取失败:${result.message}`)
|
|
88
|
+
return success(
|
|
89
|
+
`拉取完成:新增 ${result.addedSessions} 个会话(${result.addedFiles} 个文件),`
|
|
90
|
+
+ `跳过 ${result.skippedFiles} 个本地已存在的文件。`,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
if (argument === 'push') {
|
|
94
|
+
const result = await sync.push()
|
|
95
|
+
if (result.ok === false) return failure(`推送失败:${result.message}`)
|
|
96
|
+
const text = result.pushed
|
|
97
|
+
? `推送完成:${result.sessions} 个会话(${result.files} 个文件)。`
|
|
98
|
+
: '推送完成:远程已是最新,无需更新。'
|
|
99
|
+
return success(result.warning ? `${text}\n注意:${result.warning}` : text)
|
|
100
|
+
}
|
|
101
|
+
if (argument === 'test') {
|
|
102
|
+
const settings = await sync.readSettings()
|
|
103
|
+
if (!settings.remote) return failure('尚未配置 Git 地址,请先在设置页面填写。')
|
|
104
|
+
const result = await sync.test(settings.remote)
|
|
105
|
+
if (result.ok === false) return failure(`连接失败:${result.message}`)
|
|
106
|
+
return success(`连接成功:远程有 ${result.branches.length} 个分支。`)
|
|
107
|
+
}
|
|
108
|
+
return failure(`未知参数 “${argument}”。用法: /sync | /sync pull | /sync push`)
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return failure(`同步失败:${error instanceof Error ? error.message : String(error)}`)
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
}), 'dsh-sync: /sync command')
|
|
114
|
+
|
|
115
|
+
const withStatus = async (result) => {
|
|
116
|
+
let status
|
|
117
|
+
try {
|
|
118
|
+
status = await sync.status()
|
|
119
|
+
} catch {
|
|
120
|
+
status = undefined
|
|
121
|
+
}
|
|
122
|
+
return status === undefined ? result : { ...result, status }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const dispose = registerRemoteService({
|
|
126
|
+
ctx,
|
|
127
|
+
serviceKey: SERVICE_KEY,
|
|
128
|
+
namespace: NAMESPACE,
|
|
129
|
+
methods: [
|
|
130
|
+
{
|
|
131
|
+
method: 'status',
|
|
132
|
+
implementation: function status() {
|
|
133
|
+
return sync.status()
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
method: 'save',
|
|
138
|
+
implementation: async function save(remote, branch) {
|
|
139
|
+
const normalizedBranch = validateBranch(branch)
|
|
140
|
+
const result = await sync.save(remote, normalizedBranch)
|
|
141
|
+
return withStatus(result)
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
method: 'test',
|
|
146
|
+
implementation: async function test(remote) {
|
|
147
|
+
return sync.test(remote)
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
method: 'pull',
|
|
152
|
+
implementation: async function pull() {
|
|
153
|
+
return withStatus(await sync.pull())
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
method: 'push',
|
|
158
|
+
implementation: async function push() {
|
|
159
|
+
return withStatus(await sync.push())
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
})
|
|
164
|
+
ctx.effect(() => dispose, 'dsh-sync: remote service')
|
|
165
|
+
|
|
166
|
+
ctx.logger?.info?.(`dsh-sync ready: sessions=${sync.sessionsRoot}`)
|
|
167
|
+
}
|
package/lib/remote.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal host-side Remote service for the browser half.
|
|
3
|
+
*
|
|
4
|
+
* The Harness Web client reaches a plugin's host code through the Typert
|
|
5
|
+
* gateway (`ctx.connection.rpc.call('/api', '<namespace>/<method>', { args })`).
|
|
6
|
+
* The gateway only routes to a registered Cordis service that carries a
|
|
7
|
+
* `typertRemote` binding and the source-mode method descriptor on its
|
|
8
|
+
* prototype. Building that by hand keeps this plugin dependency-free: it uses
|
|
9
|
+
* only `ctx.reflect.provide` (a Cordis core service) and the documented
|
|
10
|
+
* descriptor key.
|
|
11
|
+
*
|
|
12
|
+
* A method is addressed as `<namespace>/<method>`, and the gateway spreads the
|
|
13
|
+
* request's `args` object across the implementation's parameters, matching
|
|
14
|
+
* names exactly. Keep one parameter per wire field.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Descriptor key the gateway reads off the service prototype. */
|
|
18
|
+
const REMOTE_METHODS = '@deepseek-ai/dsh-typert-protocol/remote-methods'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register one Remote service.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} deps
|
|
24
|
+
* @param {import('@deepseek-ai/cordis').Context} deps.ctx owning context
|
|
25
|
+
* @param {string} deps.serviceKey Cordis service key
|
|
26
|
+
* @param {string} deps.namespace wire namespace used in the endpoint
|
|
27
|
+
* @param {Array<{ method: string, implementation: Function }>} deps.methods
|
|
28
|
+
* @returns {() => void} disposer that unregisters the service
|
|
29
|
+
*/
|
|
30
|
+
export function registerRemoteService({ ctx, serviceKey, namespace, methods }) {
|
|
31
|
+
const service = {}
|
|
32
|
+
Object.defineProperty(service, 'ctx', { value: ctx, enumerable: false, writable: true })
|
|
33
|
+
Object.defineProperty(service, 'name', { value: serviceKey, enumerable: false, writable: true })
|
|
34
|
+
Object.defineProperty(service, 'typertRemote', {
|
|
35
|
+
value: Object.freeze({ service, serviceKey, namespace }),
|
|
36
|
+
enumerable: false,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const prototype = {}
|
|
40
|
+
for (const { method, implementation } of methods) {
|
|
41
|
+
Object.defineProperty(prototype, method, {
|
|
42
|
+
value: implementation,
|
|
43
|
+
enumerable: false,
|
|
44
|
+
writable: false,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
Object.defineProperty(prototype, REMOTE_METHODS, {
|
|
48
|
+
value: Object.freeze({
|
|
49
|
+
version: 1,
|
|
50
|
+
methods: Object.freeze(
|
|
51
|
+
methods.map(({ method }) => Object.freeze({
|
|
52
|
+
method,
|
|
53
|
+
invocation: Object.freeze({ kind: 'direct' }),
|
|
54
|
+
})),
|
|
55
|
+
),
|
|
56
|
+
}),
|
|
57
|
+
enumerable: false,
|
|
58
|
+
})
|
|
59
|
+
Object.setPrototypeOf(service, prototype)
|
|
60
|
+
|
|
61
|
+
return ctx.reflect.provide(serviceKey, service)
|
|
62
|
+
}
|