dsh-vscode-mode 0.10.0 → 0.12.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/README.md +62 -11
- package/lib/client.js +2879 -1030
- package/lib/client.js.map +1 -1
- package/lib/index.js +2227 -1056
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/activePathStore.ts +28 -0
- package/src/client/commandBridge.ts +9 -41
- package/src/client/dap/hover.ts +2 -7
- package/src/client/index.ts +116 -14
- package/src/client/keybindings.ts +19 -73
- package/src/client/luaReservedWords.ts +10 -0
- package/src/client/monaco/lsp/keywords.ts +121 -0
- package/src/client/monaco/lsp/lspClient.ts +68 -6
- package/src/client/monaco/lsp/memberScope.ts +90 -0
- package/src/client/monaco/lsp/providers.ts +14 -4
- package/src/client/settingsDocRouter.ts +52 -0
- package/src/client/shortcutsOfficial.ts +451 -0
- package/src/client/sidebar/menuItems.ts +1 -0
- package/src/client/sidebar/panels/RulesPanel.ts +15 -11
- package/src/client/sidebar/types.ts +2 -0
- package/src/client/styles/editor.css +143 -37
- package/src/client/svnActions.ts +4 -4
- package/src/client/ui/ContextMenu.ts +4 -2
- package/src/client/ui/EditorView.ts +142 -135
- package/src/client/ui/McpSettings.ts +4 -5
- package/src/client/ui/QuickOpen.ts +10 -11
- package/src/client/ui/ShortcutSettings.ts +294 -0
- package/src/client/ui/SvnLogDialog.ts +200 -69
- package/src/client/ui/SvnUpdateDialog.ts +472 -0
- package/src/client/ui/commandCatalog.ts +36 -41
- package/src/client/ui/svnUpdateModel.ts +135 -0
- package/src/fileOpenSettings.ts +3 -0
- package/src/index.ts +2 -0
- package/src/lsp/completionCap.ts +80 -0
- package/src/lsp/manager.ts +42 -10
- package/src/lsp/rpc.ts +8 -5
- package/src/lsp/server.ts +18 -4
- package/src/rpc.ts +74 -2
- package/src/rules.ts +100 -44
- package/src/settingsDocPath.ts +30 -0
- package/src/shared/keybindings.ts +253 -0
- package/src/shared/rpc.ts +12 -2
- package/src/shared/rules.ts +9 -1
- package/src/shared/svn.ts +38 -0
- package/src/svn.ts +102 -7
- package/src/svnText.ts +22 -13
- package/src/svnUpdateJob.ts +588 -0
- package/src/client/ui/KeybindingsPanel.ts +0 -190
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* `|` 连接多个候选(任一命中即触发),如 `Alt+ArrowLeft|Ctrl+Alt+-`。
|
|
6
6
|
* 约束:本模块被 host 与 client 两半共用,**禁止 import client 侧模块**(React/浏览器 API)。
|
|
7
7
|
* 与 client/ui/commandCatalog 的一致性由 tests/commands.test.ts 断言兜底。
|
|
8
|
+
* 官方机制(2026-10):SHORTCUT_PROFILES 描述各命令在官方 ctx.shortcuts 的 profile 默认键位
|
|
9
|
+
* (由 client 注册官方服务时使用);chordToBinding/bindingToChord 负责插件弦与官方绑定互转。
|
|
8
10
|
* 作者 ddj 2026年08月26号 / 2026年09月10号
|
|
9
11
|
*/
|
|
10
12
|
|
|
@@ -66,3 +68,254 @@ export function normalizeKeybindings(raw: unknown): Record<string, string> {
|
|
|
66
68
|
}
|
|
67
69
|
return out
|
|
68
70
|
}
|
|
71
|
+
|
|
72
|
+
//#region 官方 shortcuts 机制映射层(纯数据/纯函数,禁 import client 侧与官方包)
|
|
73
|
+
|
|
74
|
+
/** 官方 shortcuts 服务的修饰符标记('primary' = 桌面端按平台展开为 Ctrl/Cmd,web 同语义)。 */
|
|
75
|
+
export type OfficialModifier = 'primary' | 'control' | 'alt' | 'shift' | 'meta'
|
|
76
|
+
|
|
77
|
+
/** 官方绑定形状(物理 code + 修饰符;与 @deepseek-ai/dsh-client-shortcuts 绑定契约同构,鸭子类型)。 */
|
|
78
|
+
export interface OfficialBinding {
|
|
79
|
+
code: string
|
|
80
|
+
modifiers: OfficialModifier[]
|
|
81
|
+
secondCode?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 官方 profile 键(desktop|web × macos|windows|linux)。 */
|
|
85
|
+
export type ShortcutProfileKey =
|
|
86
|
+
| 'desktop:macos' | 'desktop:windows' | 'desktop:linux'
|
|
87
|
+
| 'web:macos' | 'web:windows' | 'web:linux'
|
|
88
|
+
|
|
89
|
+
/** 命令 id → 各 profile 官方默认键位(缺 profile = 该端不绑定)。 */
|
|
90
|
+
export type ShortcutProfiles = Partial<Record<ShortcutProfileKey, OfficialBinding>>
|
|
91
|
+
|
|
92
|
+
/** 显示主键 → 官方物理 code 对照(官方 code 白名单:Key[A-Z]|Digit[0-9]|F1-24|具名键)。 */
|
|
93
|
+
const DISPLAY_KEY_TO_CODE: Record<string, string> = {
|
|
94
|
+
'-': 'Minus', '=': 'Equal', '[': 'BracketLeft', ']': 'BracketRight',
|
|
95
|
+
';': 'Semicolon', "'": 'Quote', ',': 'Comma', '.': 'Period',
|
|
96
|
+
'/': 'Slash', '\\': 'Backslash', '`': 'Backquote',
|
|
97
|
+
Enter: 'Enter', Tab: 'Tab', Backspace: 'Backspace', Delete: 'Delete',
|
|
98
|
+
Escape: 'Escape', Space: 'Space',
|
|
99
|
+
ArrowUp: 'ArrowUp', ArrowDown: 'ArrowDown', ArrowLeft: 'ArrowLeft', ArrowRight: 'ArrowRight',
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 官方具名 code(非 Key/Digit/F 前缀且可回显的部分)。 */
|
|
103
|
+
const NAMED_CODES = ['Enter', 'Tab', 'Backspace', 'Delete', 'Escape', 'Space',
|
|
104
|
+
'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'] as const
|
|
105
|
+
|
|
106
|
+
/** 官方 code → 显示主键(符号键反向对照)。 */
|
|
107
|
+
const CODE_TO_DISPLAY_KEY: Record<string, string> = {
|
|
108
|
+
Minus: '-', Equal: '=', BracketLeft: '[', BracketRight: ']',
|
|
109
|
+
Semicolon: ';', Quote: "'", Comma: ',', Period: '.',
|
|
110
|
+
Slash: '/', Backslash: '\\', Backquote: '`',
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 显示主键 → 官方 code(PageUp/PageDown/Home/End/Insert 等官方不支持 → null)。 */
|
|
114
|
+
function keyToCode(key: string): string | null {
|
|
115
|
+
if (Object.hasOwn(DISPLAY_KEY_TO_CODE, key)) return DISPLAY_KEY_TO_CODE[key]
|
|
116
|
+
if (key.length === 1 && key >= 'a' && key <= 'z') return 'Key' + key.toUpperCase()
|
|
117
|
+
if (key.length === 1 && key >= 'A' && key <= 'Z') return 'Key' + key
|
|
118
|
+
if (key.length === 1 && key >= '0' && key <= '9') return 'Digit' + key
|
|
119
|
+
if (/^F([1-9]|1[0-9]|2[0-4])$/u.test(key)) return key
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 官方 code → 显示主键(不可回显 → null)。 */
|
|
124
|
+
function codeToKey(code: string): string | null {
|
|
125
|
+
if (Object.hasOwn(CODE_TO_DISPLAY_KEY, code)) return CODE_TO_DISPLAY_KEY[code]
|
|
126
|
+
if (/^Key[A-Z]$/u.test(code)) return code.slice(3)
|
|
127
|
+
if (/^Digit[0-9]$/u.test(code)) return code.slice(5)
|
|
128
|
+
if (/^F([1-9]|1[0-9]|2[0-4])$/u.test(code)) return code
|
|
129
|
+
if ((NAMED_CODES as readonly string[]).includes(code)) return code
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 插件弦的修饰符 token → 官方修饰符(Ctrl 与 Cmd 同为 primary,与 matchEvent 的互认语义一致)。 */
|
|
134
|
+
const CHORD_MODIFIER_TOKENS: Record<string, OfficialModifier> = {
|
|
135
|
+
ctrl: 'primary', control: 'primary', cmd: 'primary', meta: 'primary',
|
|
136
|
+
shift: 'shift', alt: 'alt',
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 插件键位弦(单候选)→ 官方绑定。
|
|
141
|
+
* 不可表示的主键(PageUp/PageDown/Home/End/Insert 等)返回 null,调用方跳过。
|
|
142
|
+
* @author ddj 2026年10月
|
|
143
|
+
* @param chord 键位弦(如 `Ctrl+Alt+-` / `F9` / `Alt+ArrowLeft`)
|
|
144
|
+
* @returns 官方绑定或 null
|
|
145
|
+
*/
|
|
146
|
+
export function chordToBinding(chord: string): OfficialBinding | null {
|
|
147
|
+
const parts = String(chord ?? '').split('+').map((part) => part.trim()).filter(Boolean)
|
|
148
|
+
if (!parts.length) return null
|
|
149
|
+
const modifiers = new Set<OfficialModifier>()
|
|
150
|
+
let key = ''
|
|
151
|
+
for (const part of parts) {
|
|
152
|
+
const modifier = CHORD_MODIFIER_TOKENS[part.toLowerCase()]
|
|
153
|
+
if (modifier) {
|
|
154
|
+
modifiers.add(modifier)
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
if (key) return null
|
|
158
|
+
key = part
|
|
159
|
+
}
|
|
160
|
+
if (!key) return null
|
|
161
|
+
if (key === ' ') key = 'Space'
|
|
162
|
+
else if (key.length === 1 && key >= 'a' && key <= 'z') key = key.toUpperCase()
|
|
163
|
+
const code = keyToCode(key)
|
|
164
|
+
return code === null ? null : { code, modifiers: [...modifiers] }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 官方绑定 → 插件显示键位弦(primary 显示为 Ctrl,与历史 tooltip 文案保持一致)。
|
|
169
|
+
* @author ddj 2026年10月
|
|
170
|
+
* @param binding 官方绑定
|
|
171
|
+
* @returns 插件弦(不可表示 → null)
|
|
172
|
+
*/
|
|
173
|
+
export function bindingToChord(binding: OfficialBinding): string | null {
|
|
174
|
+
if (!binding || typeof binding !== 'object') return null
|
|
175
|
+
const key = codeToKey(binding.code)
|
|
176
|
+
if (key === null) return null
|
|
177
|
+
let suffix = ''
|
|
178
|
+
if (binding.secondCode !== undefined) {
|
|
179
|
+
const secondKey = codeToKey(binding.secondCode)
|
|
180
|
+
if (secondKey === null) return null
|
|
181
|
+
suffix = '+' + secondKey
|
|
182
|
+
}
|
|
183
|
+
const parts: string[] = []
|
|
184
|
+
if (binding.modifiers.includes('primary')) parts.push('Ctrl')
|
|
185
|
+
if (binding.modifiers.includes('shift')) parts.push('Shift')
|
|
186
|
+
if (binding.modifiers.includes('alt')) parts.push('Alt')
|
|
187
|
+
return [...parts, key + suffix].join('+')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 官方绑定字面量构造(表格可读性辅助)。 */
|
|
191
|
+
function bind(code: string, modifiers: OfficialModifier[]): OfficialBinding {
|
|
192
|
+
return { code, modifiers }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 命令 id → 官方 profile 默认键位(与 KEYBINDING_DEFAULTS 一一对应;client 注册官方服务的数据源)。
|
|
197
|
+
* 取值约束(对齐官方 bindingIssue/isWebBindingAllowed 校验,错报会在注册时抛错):
|
|
198
|
+
* - desktop:macos/windows:官方不做保留校验,用 VS Code 同款 chord;
|
|
199
|
+
* - desktop:linux / web:linux:Arrow*、裸 F 键、全 Shift、Ctrl+V/C/X/Z/Y/Q/H/A、Meta 均为保留键 → 不声明;
|
|
200
|
+
* - web:macos/windows:单修饰符组合不在白名单 → 用 primary+alt / primary+shift 变体(官方 ui-layout 同款);
|
|
201
|
+
* - Ctrl+PageDown/PageUp 候选废弃(官方 code 白名单无 PageUp/PageDown);
|
|
202
|
+
* - edrv.toggleSidebar 留空:Ctrl+B 与官方 sidebar.left.toggle(Mod+B)冲突,注册会抛错且冲突键互相阻断。
|
|
203
|
+
* @author ddj 2026年10月
|
|
204
|
+
*/
|
|
205
|
+
export const SHORTCUT_PROFILES: Record<string, ShortcutProfiles> = {
|
|
206
|
+
'edrv.toggleSidebar': {},
|
|
207
|
+
'edrv.save': {
|
|
208
|
+
'desktop:macos': bind('KeyS', ['primary']),
|
|
209
|
+
'desktop:windows': bind('KeyS', ['primary']),
|
|
210
|
+
'desktop:linux': bind('KeyS', ['primary']),
|
|
211
|
+
},
|
|
212
|
+
// 官方 workspace.files 已占用桌面 primary+P 和 Web primary+alt+P;插件 QuickOpen 使用非冲突组合。
|
|
213
|
+
'edrv.quickOpen': {
|
|
214
|
+
'desktop:macos': bind('KeyP', ['primary', 'alt']),
|
|
215
|
+
'desktop:windows': bind('KeyP', ['primary', 'alt']),
|
|
216
|
+
'desktop:linux': bind('KeyP', ['primary', 'alt']),
|
|
217
|
+
'web:macos': bind('KeyP', ['primary', 'alt', 'shift']),
|
|
218
|
+
'web:windows': bind('KeyP', ['primary', 'alt', 'shift']),
|
|
219
|
+
},
|
|
220
|
+
'edrv.searchInFiles': {
|
|
221
|
+
'desktop:macos': bind('KeyF', ['primary', 'shift']),
|
|
222
|
+
'desktop:windows': bind('KeyF', ['primary', 'shift']),
|
|
223
|
+
'desktop:linux': bind('KeyF', ['primary', 'shift']),
|
|
224
|
+
'web:macos': bind('KeyF', ['primary', 'shift']),
|
|
225
|
+
'web:windows': bind('KeyF', ['primary', 'shift']),
|
|
226
|
+
},
|
|
227
|
+
// Ctrl+Shift+V(primary+KeyV)在 linux 为保留键(官方 bindingIssue 不豁免 shift),desktop:linux 不声明
|
|
228
|
+
'edrv.toggleMarkdownPreview': {
|
|
229
|
+
'desktop:macos': bind('KeyV', ['primary', 'shift']),
|
|
230
|
+
'desktop:windows': bind('KeyV', ['primary', 'shift']),
|
|
231
|
+
'web:macos': bind('KeyV', ['primary', 'shift']),
|
|
232
|
+
'web:windows': bind('KeyV', ['primary', 'shift']),
|
|
233
|
+
},
|
|
234
|
+
// 导航历史:主候选 Alt+Arrow 在 linux/web 为保留或白名单外;web 取第二候选(Ctrl+Alt+- / Ctrl+Shift+-)
|
|
235
|
+
'edrv.navigateBack': {
|
|
236
|
+
'desktop:macos': bind('ArrowLeft', ['alt']),
|
|
237
|
+
'desktop:windows': bind('ArrowLeft', ['alt']),
|
|
238
|
+
'web:macos': bind('Minus', ['primary', 'alt']),
|
|
239
|
+
'web:windows': bind('Minus', ['primary', 'alt']),
|
|
240
|
+
},
|
|
241
|
+
'edrv.navigateForward': {
|
|
242
|
+
'desktop:macos': bind('ArrowRight', ['alt']),
|
|
243
|
+
'desktop:windows': bind('ArrowRight', ['alt']),
|
|
244
|
+
'web:macos': bind('Minus', ['primary', 'shift']),
|
|
245
|
+
'web:windows': bind('Minus', ['primary', 'shift']),
|
|
246
|
+
},
|
|
247
|
+
// 页签循环:Ctrl+PageDown/PageUp 候选官方不可表示,仅保留 Ctrl+Alt+Arrow 主候选
|
|
248
|
+
'edrv.nextTab': {
|
|
249
|
+
'desktop:macos': bind('ArrowRight', ['primary', 'alt']),
|
|
250
|
+
'desktop:windows': bind('ArrowRight', ['primary', 'alt']),
|
|
251
|
+
'web:macos': bind('ArrowRight', ['primary', 'alt']),
|
|
252
|
+
'web:windows': bind('ArrowRight', ['primary', 'alt']),
|
|
253
|
+
},
|
|
254
|
+
'edrv.prevTab': {
|
|
255
|
+
'desktop:macos': bind('ArrowLeft', ['primary', 'alt']),
|
|
256
|
+
'desktop:windows': bind('ArrowLeft', ['primary', 'alt']),
|
|
257
|
+
'web:macos': bind('ArrowLeft', ['primary', 'alt']),
|
|
258
|
+
'web:windows': bind('ArrowLeft', ['primary', 'alt']),
|
|
259
|
+
},
|
|
260
|
+
'edrv.goToLine': {
|
|
261
|
+
'desktop:macos': bind('KeyG', ['primary']),
|
|
262
|
+
'desktop:windows': bind('KeyG', ['primary']),
|
|
263
|
+
'desktop:linux': bind('KeyG', ['primary']),
|
|
264
|
+
},
|
|
265
|
+
'edrv.showCommands': {
|
|
266
|
+
'desktop:macos': bind('KeyP', ['primary', 'shift']),
|
|
267
|
+
'desktop:windows': bind('KeyP', ['primary', 'shift']),
|
|
268
|
+
'desktop:linux': bind('KeyP', ['primary', 'shift']),
|
|
269
|
+
'web:macos': bind('KeyP', ['primary', 'shift']),
|
|
270
|
+
'web:windows': bind('KeyP', ['primary', 'shift']),
|
|
271
|
+
},
|
|
272
|
+
'edrv.nextEditorRow': {
|
|
273
|
+
'desktop:macos': bind('ArrowDown', ['primary', 'alt']),
|
|
274
|
+
'desktop:windows': bind('ArrowDown', ['primary', 'alt']),
|
|
275
|
+
'web:macos': bind('ArrowDown', ['primary', 'alt']),
|
|
276
|
+
'web:windows': bind('ArrowDown', ['primary', 'alt']),
|
|
277
|
+
},
|
|
278
|
+
'edrv.prevEditorRow': {
|
|
279
|
+
'desktop:macos': bind('ArrowUp', ['primary', 'alt']),
|
|
280
|
+
'desktop:windows': bind('ArrowUp', ['primary', 'alt']),
|
|
281
|
+
'web:macos': bind('ArrowUp', ['primary', 'alt']),
|
|
282
|
+
'web:windows': bind('ArrowUp', ['primary', 'alt']),
|
|
283
|
+
},
|
|
284
|
+
'edrv.addSelectionRef': {
|
|
285
|
+
'desktop:macos': bind('KeyU', ['primary']),
|
|
286
|
+
'desktop:windows': bind('KeyU', ['primary']),
|
|
287
|
+
'desktop:linux': bind('KeyU', ['primary']),
|
|
288
|
+
},
|
|
289
|
+
'edrv.closeTab': {
|
|
290
|
+
'desktop:macos': bind('F4', ['primary']),
|
|
291
|
+
'desktop:windows': bind('F4', ['primary']),
|
|
292
|
+
'desktop:linux': bind('F4', ['primary']),
|
|
293
|
+
},
|
|
294
|
+
// 调试 F 键:linux 判 modifier-required(裸 F/全 Shift),仅 desktop:macos/windows
|
|
295
|
+
'edrv.debugToggleBreakpoint': {
|
|
296
|
+
'desktop:macos': bind('F9', []),
|
|
297
|
+
'desktop:windows': bind('F9', []),
|
|
298
|
+
},
|
|
299
|
+
'edrv.debugStartContinue': {
|
|
300
|
+
'desktop:macos': bind('F5', []),
|
|
301
|
+
'desktop:windows': bind('F5', []),
|
|
302
|
+
},
|
|
303
|
+
'edrv.debugStepOver': {
|
|
304
|
+
'desktop:macos': bind('F10', []),
|
|
305
|
+
'desktop:windows': bind('F10', []),
|
|
306
|
+
},
|
|
307
|
+
'edrv.debugStepInto': {
|
|
308
|
+
'desktop:macos': bind('F11', []),
|
|
309
|
+
'desktop:windows': bind('F11', []),
|
|
310
|
+
},
|
|
311
|
+
'edrv.debugStepOut': {
|
|
312
|
+
'desktop:macos': bind('F11', ['shift']),
|
|
313
|
+
'desktop:windows': bind('F11', ['shift']),
|
|
314
|
+
},
|
|
315
|
+
'edrv.debugStop': {
|
|
316
|
+
'desktop:macos': bind('F5', ['shift']),
|
|
317
|
+
'desktop:windows': bind('F5', ['shift']),
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
//#endregion
|
package/src/shared/rpc.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type { RuleInfo, RuleProject, RuleRefInput, RuleSaveInput } from './rules
|
|
|
19
19
|
import type { SnippetEntry, SnippetInfo, SnippetProject, SnippetRefInput, SnippetSaveInput } from './snippets.js'
|
|
20
20
|
import type { LspCompletionItem, LspCompletionList, LspEnvInstallState, LspExtInfo, LspExtUpdate, LspHover, LspLocation, LspMarketItem, LspPosition, LspSemanticTokens, LspServerStatus, LspSignatureHelp, LspSymbol } from './lsp.js'
|
|
21
21
|
import type { AiConfigPatch, AiConfigView, AiDirectoryView, AiInlineRequest, AiInlineResult } from './ai.js'
|
|
22
|
-
import type { SvnAction, SvnAiPlan, SvnChangeEntry, SvnConflictArtifact, SvnDiffRevResult, SvnIgnoreItem, SvnLogEntry, SvnRemoteOutdatedEntry, SvnStatusPayload, SvnSumEntry, SvnUpdateResult } from './svn.js'
|
|
22
|
+
import type { SvnAction, SvnAiPlan, SvnChangeEntry, SvnConflictArtifact, SvnDiffRevResult, SvnIgnoreItem, SvnLogEntry, SvnRemoteOutdatedEntry, SvnStatusPayload, SvnSumEntry, SvnUpdatePoll, SvnUpdateResult, SvnUpdateState } from './svn.js'
|
|
23
23
|
import type { DapAction, DapAdapterInfo, DapBreakpointAck, DapBreakpointInput, DapConfigSnippet, DapConfigSource, DapDebugConfig, DapFrameView, DapPhase, DapPollResult, DapProcessInfo, DapScopeView, DapVariableView } from './dap.js'
|
|
24
24
|
import { DAP_ACTIONS } from './dap.js'
|
|
25
25
|
import type { LoggerLevel } from './logger.js'
|
|
@@ -371,6 +371,7 @@ export interface RpcRequestMap {
|
|
|
371
371
|
'mcp.projectToggle': { workspacePath: string; serverName: string; enabled: boolean }
|
|
372
372
|
'mcp.projectRefresh': { workspacePath: string; serverName: string }
|
|
373
373
|
'vscode.fileOpenSettingsGet': {}
|
|
374
|
+
'vscode.settingsDocumentPath': {}
|
|
374
375
|
'vscode.fileOpenSettingsUpdate': { fileOpenTool: string; integrationBaseUrl?: string; expectedRevision?: number }
|
|
375
376
|
'edrv.integration.status': {}
|
|
376
377
|
'edrv.integration.register': {}
|
|
@@ -396,7 +397,7 @@ export interface RpcRequestMap {
|
|
|
396
397
|
'edrv.lsp.workspaceSymbol': { sessionId?: string; query: string }
|
|
397
398
|
'edrv.lsp.hover': { sessionId?: string; path: string; position: LspPosition }
|
|
398
399
|
'edrv.lsp.semanticTokens': { sessionId?: string; path: string }
|
|
399
|
-
'edrv.lsp.completion': { sessionId?: string; path: string; position: LspPosition; context?: { triggerKind?: number; triggerCharacter?: string } }
|
|
400
|
+
'edrv.lsp.completion': { sessionId?: string; path: string; position: LspPosition; context?: { triggerKind?: number; triggerCharacter?: string }; prefix?: string }
|
|
400
401
|
'edrv.lsp.resolveCompletion': { sessionId?: string; path: string; item: LspCompletionItem }
|
|
401
402
|
'edrv.lsp.signatureHelp': { sessionId?: string; path: string; position: LspPosition }
|
|
402
403
|
'edrv.lsp.redetect': { languageId: string }
|
|
@@ -447,6 +448,10 @@ export interface RpcRequestMap {
|
|
|
447
448
|
'svn.wcRev': { sessionId?: string; path?: string }
|
|
448
449
|
'svn.cleanup': { sessionId?: string; removeUnversioned?: boolean; removeIgnored?: boolean }
|
|
449
450
|
'svn.update': { sessionId?: string; path?: string }
|
|
451
|
+
'svn.updateStart': { sessionId?: string; path?: string }
|
|
452
|
+
'svn.updatePoll': { sessionId?: string; jobId: string; since: number }
|
|
453
|
+
'svn.updateCancel': { sessionId?: string; jobId: string }
|
|
454
|
+
'svn.updateActive': { sessionId?: string }
|
|
450
455
|
'svn.tortoise': { sessionId?: string; action: SvnAction; path?: string }
|
|
451
456
|
'svn.patchText': { sessionId?: string; path: string; whitespace?: 'none' | 'b' | 'w'; ignoreEol?: boolean; unified?: number }
|
|
452
457
|
'svn.diffSum': { sessionId?: string; path?: string; revA: number; revB: number }
|
|
@@ -519,6 +524,7 @@ export interface RpcOkMap {
|
|
|
519
524
|
'mcp.projectToggle': { project: MpcProject }
|
|
520
525
|
'mcp.projectRefresh': { project: MpcProject }
|
|
521
526
|
'vscode.fileOpenSettingsGet': { fileOpenTool: string; integrationBaseUrl: string; revision?: number }
|
|
527
|
+
'vscode.settingsDocumentPath': { path: string }
|
|
522
528
|
'vscode.fileOpenSettingsUpdate': { fileOpenTool: string; integrationBaseUrl: string; revision?: number }
|
|
523
529
|
'edrv.integration.status': ShellIntegrationStatus
|
|
524
530
|
'edrv.integration.register': ShellIntegrationStatus
|
|
@@ -595,6 +601,10 @@ export interface RpcOkMap {
|
|
|
595
601
|
'svn.wcRev': { revision: number | null }
|
|
596
602
|
'svn.cleanup': { summary: string; output: string }
|
|
597
603
|
'svn.update': SvnUpdateResult
|
|
604
|
+
'svn.updateStart': SvnUpdateState
|
|
605
|
+
'svn.updatePoll': SvnUpdatePoll
|
|
606
|
+
'svn.updateCancel': SvnUpdateState
|
|
607
|
+
'svn.updateActive': { job: SvnUpdateState | null }
|
|
598
608
|
'svn.tortoise': { launched: string }
|
|
599
609
|
'svn.patchText': { text: string; truncated: boolean; binary: boolean }
|
|
600
610
|
'svn.diffSum': { entries: SvnSumEntry[]; truncated: boolean }
|
package/src/shared/rules.ts
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* 作者 ddj 2026年09月03号
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/** 用户规则来源:当前 DSH_HOME 或系统用户目录的旧版 .dsh。 */
|
|
8
|
+
export type UserRuleOrigin = 'current' | 'legacy'
|
|
9
|
+
|
|
10
|
+
/** 规则作用域:用户规则(DSH_HOME/rules + ~/.dsh/rules)或项目规则(<工作区>/.dsh/rules)。 */
|
|
8
11
|
export type RuleScope = 'user' | 'project'
|
|
9
12
|
|
|
10
13
|
/** 规则生效类型(由 frontmatter 推导):总是 / 自动(globs 命中时)/ 手动(仅索引)。 */
|
|
@@ -13,6 +16,8 @@ export type RuleType = 'always' | 'auto' | 'manual'
|
|
|
13
16
|
/** 一条 .mdc 规则的元信息(列表行展示 + 注入语义所需的最小集)。 */
|
|
14
17
|
export interface RuleInfo {
|
|
15
18
|
scope: RuleScope
|
|
19
|
+
/** 用户规则的实际来源;项目规则不设置。 */
|
|
20
|
+
origin?: UserRuleOrigin
|
|
16
21
|
/** 文件名(含 .mdc 后缀,不含路径)。 */
|
|
17
22
|
file: string
|
|
18
23
|
/** 绝对路径(host 解析,供展示与按需读取)。 */
|
|
@@ -48,6 +53,8 @@ export interface RuleSaveInput {
|
|
|
48
53
|
scope: RuleScope
|
|
49
54
|
/** project 必填:目标工作区绝对路径(须为 DSH 已注册 workspace)。 */
|
|
50
55
|
workspacePath?: string
|
|
56
|
+
/** 缺省 current;仅用户规则可指定 legacy。 */
|
|
57
|
+
origin?: UserRuleOrigin
|
|
51
58
|
file: string
|
|
52
59
|
content: string
|
|
53
60
|
}
|
|
@@ -56,5 +63,6 @@ export interface RuleSaveInput {
|
|
|
56
63
|
export interface RuleRefInput {
|
|
57
64
|
scope: RuleScope
|
|
58
65
|
workspacePath?: string
|
|
66
|
+
origin?: UserRuleOrigin
|
|
59
67
|
file: string
|
|
60
68
|
}
|
package/src/shared/svn.ts
CHANGED
|
@@ -99,6 +99,44 @@ export interface SvnUpdateResult {
|
|
|
99
99
|
output: string
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/** 原生更新窗口的真实执行阶段。 */
|
|
103
|
+
export type SvnUpdatePhase = 'starting' | 'running' | 'cancelling' | 'completed' | 'failed' | 'cancelled'
|
|
104
|
+
|
|
105
|
+
/** 流式更新行;relPath 仅在目标位于当前工作区内时可操作。 */
|
|
106
|
+
export interface SvnUpdateRow {
|
|
107
|
+
seq: number
|
|
108
|
+
action: SvnUpdateAction | null
|
|
109
|
+
path: string
|
|
110
|
+
relPath: string | null
|
|
111
|
+
label: string
|
|
112
|
+
raw: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 分类数量按 svn 输出中的条目行计,不猜文件总数或网络字节。 */
|
|
116
|
+
export type SvnUpdateCounts = Record<SvnUpdateAction, number>
|
|
117
|
+
|
|
118
|
+
/** 更新任务快照(窗口第一次连接和每次增量 poll 共用)。 */
|
|
119
|
+
export interface SvnUpdateState {
|
|
120
|
+
jobId: string
|
|
121
|
+
target: string
|
|
122
|
+
phase: SvnUpdatePhase
|
|
123
|
+
counts: SvnUpdateCounts
|
|
124
|
+
revision: number | null
|
|
125
|
+
elapsedMs: number
|
|
126
|
+
error: string
|
|
127
|
+
rawTail: string
|
|
128
|
+
truncated: boolean
|
|
129
|
+
nextSeq: number
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 增量读取结果:rows 只含 since 之后的一页,offset 表示目前可重放的首条序号。 */
|
|
133
|
+
export interface SvnUpdatePoll extends SvnUpdateState {
|
|
134
|
+
rows: SvnUpdateRow[]
|
|
135
|
+
offset: number
|
|
136
|
+
/** 当前后台总序号;nextSeq 仅是本次前端已经拿到的最后一条。 */
|
|
137
|
+
totalSeq: number
|
|
138
|
+
}
|
|
139
|
+
|
|
102
140
|
/** revert/add 等批量动作结果(计数 + 摘要 + 原文)。 */
|
|
103
141
|
export interface SvnActionResult {
|
|
104
142
|
/** 处理条目数(宽松计数)。 */
|
package/src/svn.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 作者 ddj 2026年09月16号
|
|
8
8
|
*/
|
|
9
9
|
import { readdir, stat } from 'node:fs/promises'
|
|
10
|
-
import { basename, dirname, join } from 'node:path'
|
|
10
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
11
11
|
import { tmpdir } from 'node:os'
|
|
12
12
|
import type { Ctx, Session } from './store.js'
|
|
13
13
|
import { cwdOf, sessionOf } from './registry.js'
|
|
@@ -18,6 +18,8 @@ import { buildAiPrompt, svnAiPlanOf } from './ai/svnTriage.js'
|
|
|
18
18
|
import { attrOf, numAttrOf, scanXmlTags, textOf, unescapeXml } from './svnXml.js'
|
|
19
19
|
import { TEXT_OUTPUT_CAP, batchResultOf, tailOfText, updateResultOf } from './svnText.js'
|
|
20
20
|
import { LOG_CAP, LOG_DEFAULT_LIMIT, SVN_LOG_SHOW_ALL_CAP, parseLogXml, parseSvnInfoRevision } from './svnLog.js'
|
|
21
|
+
import { SvnUpdateJobs } from './svnUpdateJob.js'
|
|
22
|
+
import type { UpdateProcess } from './svnUpdateJob.js'
|
|
21
23
|
export { batchResultOf, commitResultOf, updateResultOf } from './svnText.js'
|
|
22
24
|
export { mapRepoPath, parseLogXml } from './svnLog.js'
|
|
23
25
|
|
|
@@ -387,6 +389,7 @@ interface SvnRunOutcome { code: number | null; stdout: string; stderr: string }
|
|
|
387
389
|
/** subprocess 产句柄最小结构面(done 载荷对齐 revert.ts 的 exitCode/code 双读)。 */
|
|
388
390
|
interface SpawnedHandle {
|
|
389
391
|
done: Promise<{ exitCode?: number; code?: number } | undefined>
|
|
392
|
+
terminate?: () => void
|
|
390
393
|
collected?: {
|
|
391
394
|
stdout?: { readFrom: (n: number) => { text: string } }
|
|
392
395
|
stderr?: { readFrom: (n: number) => { text: string } }
|
|
@@ -466,18 +469,35 @@ function safeRel(raw: unknown): string | null {
|
|
|
466
469
|
return rel
|
|
467
470
|
}
|
|
468
471
|
|
|
472
|
+
/**
|
|
473
|
+
* 会话根可在 SVN 工作副本的子目录;CLI 在 wcRoot 执行时必须换算目标坐标。
|
|
474
|
+
* @author ddj 2026年09月24号
|
|
475
|
+
* @param cwd 当前会话目录
|
|
476
|
+
* @param wcRoot SVN 工作副本根
|
|
477
|
+
* @param rel 会话相对目标
|
|
478
|
+
* @returns SVN 根相对目标;越界返回 null
|
|
479
|
+
*/
|
|
480
|
+
export function svnPathOf(cwd: string, wcRoot: string, rel: string): string | null {
|
|
481
|
+
if (safeRel(rel) === null) return null
|
|
482
|
+
const wcRel = relative(wcRoot, resolve(cwd, rel || '.'))
|
|
483
|
+
if (isAbsolute(wcRel) || wcRel === '..' || wcRel.startsWith('../') || wcRel.startsWith('..\\')) return null
|
|
484
|
+
return wcRel.replace(/\\/g, '/') || '.'
|
|
485
|
+
}
|
|
486
|
+
|
|
469
487
|
/**
|
|
470
488
|
* 创建 svn.* RPC handlers(缓存随装配生命周期;lsp/ai 同模式并入 buildHandlers)。
|
|
471
489
|
* @author ddj 2026年09月16号
|
|
472
490
|
* @param deps ctx + 设置读取面 + 可注入时钟
|
|
473
491
|
* @returns handlers(Partial<RpcHandlerMap> 形状)
|
|
474
492
|
*/
|
|
475
|
-
export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unknown
|
|
493
|
+
export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unknown>; dispose: () => void } {
|
|
476
494
|
const { ctx, settings } = deps
|
|
477
495
|
const now = deps.now ?? (() => Date.now())
|
|
478
496
|
const findRoot = deps.findRoot ?? ((cwd: string) => findSvnRoot(cwd))
|
|
479
497
|
const findTortoise = deps.findTortoise ?? ((dir: string) => findTortoiseProc(dir))
|
|
480
498
|
const statusCache = new Map<string, { at: number; payload: SvnStatusPayload }>()
|
|
499
|
+
/** 运行中更新按会话目录复用工作副本根;TTL 到期才重新扫描 .svn。 */
|
|
500
|
+
const updateRootCache = new Map<string, { at: number; root: string }>()
|
|
481
501
|
/** 变更清单缓存(键 = 工作副本根;TTL 短,仅合并同一轮 UI 的多处拉取)。 */
|
|
482
502
|
const changesCache = new Map<string, { at: number; value: { entries: SvnChangeEntry[]; truncated: boolean } }>()
|
|
483
503
|
/** 混合通道方案收件箱(key = 工作副本根;覆盖式入箱 + TTL,见 svn.aiPlanSubmit/Pending)。 */
|
|
@@ -485,6 +505,11 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
485
505
|
/** 工作副本根 relative-url 缓存(日志路径映射用;探测失败也缓存空串避免反复 spawn)。 */
|
|
486
506
|
const urlCache = new Map<string, { at: number; value: string }>()
|
|
487
507
|
let cliProbe: { at: number; exe: string; ok: boolean } | null = null
|
|
508
|
+
const updateJobs = new SvnUpdateJobs(
|
|
509
|
+
() => ctx.get('subprocess') as UpdateProcess | null,
|
|
510
|
+
deps.platform ?? process.platform,
|
|
511
|
+
now,
|
|
512
|
+
)
|
|
488
513
|
|
|
489
514
|
const subprocess = (): SpawnFn | null => {
|
|
490
515
|
const sub = ctx.get('subprocess') as { spawn?: unknown } | null | undefined
|
|
@@ -499,6 +524,9 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
499
524
|
let ok = false
|
|
500
525
|
const spawn = subprocess()
|
|
501
526
|
if (spawn) {
|
|
527
|
+
const controller = new AbortController()
|
|
528
|
+
const deadline = setTimeout(() => controller.abort(new Error('SVN CLI 探测超时')), CLI_GRACE_MS)
|
|
529
|
+
deadline.unref?.()
|
|
502
530
|
try {
|
|
503
531
|
const handle = spawn({
|
|
504
532
|
argv: [exe, '--version', '--quiet'],
|
|
@@ -507,11 +535,20 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
507
535
|
cwd: tmpdir(),
|
|
508
536
|
stdio: { stdout: 'inherit', stderr: 'inherit', stdin: 'ignore' },
|
|
509
537
|
graceMs: CLI_GRACE_MS,
|
|
538
|
+
signal: controller.signal,
|
|
539
|
+
})
|
|
540
|
+
const expired = new Promise<never>((_, reject) => {
|
|
541
|
+
controller.signal.addEventListener('abort', () => {
|
|
542
|
+
try { handle.terminate?.() } catch { /* 返回探测超时即可 */ }
|
|
543
|
+
reject(controller.signal.reason)
|
|
544
|
+
}, { once: true })
|
|
510
545
|
})
|
|
511
|
-
const outcome = await handle.done
|
|
546
|
+
const outcome = await Promise.race([handle.done, expired])
|
|
512
547
|
ok = (outcome?.exitCode ?? outcome?.code) === 0
|
|
513
548
|
} catch {
|
|
514
549
|
ok = false
|
|
550
|
+
} finally {
|
|
551
|
+
clearTimeout(deadline)
|
|
515
552
|
}
|
|
516
553
|
}
|
|
517
554
|
cliProbe = { at, exe, ok }
|
|
@@ -543,6 +580,28 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
543
580
|
return payload
|
|
544
581
|
}
|
|
545
582
|
|
|
583
|
+
/**
|
|
584
|
+
* 验证更新请求属于当前会话的工作副本;取消/轮询不依赖 CLI 仍可用。
|
|
585
|
+
* @author ddj 2026年09月24号
|
|
586
|
+
* @param sessionId 当前会话 id
|
|
587
|
+
* @param needCli 仅真正启动更新时才探测 SVN 客户端
|
|
588
|
+
* @returns 已校验的目录和状态,或错误
|
|
589
|
+
*/
|
|
590
|
+
const updateScope = async (sessionId?: string, needCli = false) => {
|
|
591
|
+
const sc = await requireSvnSession(ctx, sessionId)
|
|
592
|
+
if ('err' in sc) return { err: sc.err }
|
|
593
|
+
if (!needCli) {
|
|
594
|
+
const cached = updateRootCache.get(sc.cwd)
|
|
595
|
+
const root = cached && now() - cached.at < STATUS_TTL_MS ? cached.root : await findRoot(sc.cwd)
|
|
596
|
+
if (root && (!cached || now() - cached.at >= STATUS_TTL_MS)) updateRootCache.set(sc.cwd, { at: now(), root })
|
|
597
|
+
return root ? { cwd: sc.cwd, root, exe: '', cli: false } : { err: '当前工作区不受 SVN 管理' }
|
|
598
|
+
}
|
|
599
|
+
const status = await statusOf(sc.cwd)
|
|
600
|
+
if (!status.managed || !status.wcRoot) return { err: '当前工作区不受 SVN 管理' }
|
|
601
|
+
updateRootCache.set(sc.cwd, { at: now(), root: status.wcRoot })
|
|
602
|
+
return { cwd: sc.cwd, root: status.wcRoot, exe: status.svnPath, cli: status.svnCli }
|
|
603
|
+
}
|
|
604
|
+
|
|
546
605
|
/**
|
|
547
606
|
* 运行 svn CLI(管道收集;spawn 级失败回退 inherit,输出降级为空)。
|
|
548
607
|
* @author ddj 2026年08月26号 / 2026年09月17号
|
|
@@ -966,8 +1025,10 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
966
1025
|
const status = await statusOf(sc.cwd)
|
|
967
1026
|
if (!status.managed || !status.wcRoot) return { ok: false as const, error: '当前工作区不受 SVN 管理' }
|
|
968
1027
|
if (!status.svnCli) return { ok: false as const, error: 'svn 命令不可用(可在设置页配置 svn 路径)' }
|
|
1028
|
+
const svnTarget = svnPathOf(sc.cwd, status.wcRoot, rel)
|
|
1029
|
+
if (svnTarget === null) return { ok: false as const, error: '日志路径不在 SVN 工作副本内' }
|
|
969
1030
|
try {
|
|
970
|
-
const result = await logOf(status.wcRoot,
|
|
1031
|
+
const result = await logOf(status.wcRoot, svnTarget === '.' ? '' : svnTarget, args.limit ?? LOG_DEFAULT_LIMIT, {
|
|
971
1032
|
stopOnCopy: args.stopOnCopy === true,
|
|
972
1033
|
showMerged: args.showMerged === true,
|
|
973
1034
|
range: args.startRev && args.endRev
|
|
@@ -1026,6 +1087,8 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
1026
1087
|
const status = await statusOf(sc.cwd)
|
|
1027
1088
|
if (!status.managed || !status.wcRoot) return { ok: false as const, error: '当前工作区不受 SVN 管理' }
|
|
1028
1089
|
if (!status.svnCli) return { ok: false as const, error: 'svn 命令不可用(可在设置页配置 svn 路径)' }
|
|
1090
|
+
const svnRel = svnPathOf(sc.cwd, status.wcRoot, rel)
|
|
1091
|
+
if (!svnRel) return { ok: false as const, error: '比较路径不在 SVN 工作副本内' }
|
|
1029
1092
|
const fs = ctx.get('fs') as {
|
|
1030
1093
|
resolve?: (p: string, o: object) => Promise<unknown>
|
|
1031
1094
|
stat?: (t: unknown) => Promise<{ type?: string; size?: number } | null>
|
|
@@ -1039,7 +1102,7 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
1039
1102
|
if ((info.size ?? 0) > READ_CAP) return { ok: false as const, error: '文件过大(>8MB),不支持与工作副本比较' }
|
|
1040
1103
|
const working = await fs.readText(resolved)
|
|
1041
1104
|
// 左侧 = 该版本内容;cat 失败按「该版本尚无此文件」处理(新增语义,非错误)
|
|
1042
|
-
const leftOutcome = await runSvn(status.wcRoot, ['cat', '-r', String(Math.floor(revision)), '--',
|
|
1105
|
+
const leftOutcome = await runSvn(status.wcRoot, ['cat', '-r', String(Math.floor(revision)), '--', svnRel], READ_GRACE_MS)
|
|
1043
1106
|
if (leftOutcome.code !== 0) {
|
|
1044
1107
|
const text = (leftOutcome.stderr || leftOutcome.stdout).trim()
|
|
1045
1108
|
return { ok: true as const, left: null, right: working, reason: 'not-exist', error: tailOfText(text), ...guardSides(null, working) }
|
|
@@ -1363,7 +1426,7 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
1363
1426
|
if (!status.managed) return { ok: false as const, error: '当前工作区不受 SVN 管理' }
|
|
1364
1427
|
if (!status.svnCli) return { ok: false as const, error: 'svn 命令不可用(可在设置页配置 svn 路径)' }
|
|
1365
1428
|
try {
|
|
1366
|
-
const outcome = await runSvn(sc.cwd, ['update', rel || '.'], UPDATE_GRACE_MS)
|
|
1429
|
+
const outcome = await runSvn(sc.cwd, ['update', '--', rel || '.'], UPDATE_GRACE_MS)
|
|
1367
1430
|
const result = updateResultOf(outcome.stdout, outcome.stderr, outcome.code)
|
|
1368
1431
|
if (outcome.code !== 0) return { ok: false as const, error: '更新失败:' + tailOfText(result.output) }
|
|
1369
1432
|
return { ok: true as const, ...result }
|
|
@@ -1371,6 +1434,38 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
1371
1434
|
return { ok: false as const, error: '更新失败:' + String(error) }
|
|
1372
1435
|
}
|
|
1373
1436
|
},
|
|
1437
|
+
'svn.updateStart': async (args: { sessionId?: string; path?: string }) => {
|
|
1438
|
+
const scope = await updateScope(args.sessionId, true)
|
|
1439
|
+
if ('err' in scope) return { ok: false as const, error: scope.err }
|
|
1440
|
+
if (!scope.cli) return { ok: false as const, error: 'svn 命令不可用(可在设置页配置 svn 路径)' }
|
|
1441
|
+
const rel = safeRel(args.path)
|
|
1442
|
+
if (rel === null) return { ok: false as const, error: '路径不合法' }
|
|
1443
|
+
try {
|
|
1444
|
+
const fs = ctx.get('fs') as { resolve?: (p: string, o: object) => Promise<unknown>; processPath?: (t: unknown) => string } | undefined
|
|
1445
|
+
const cwd = fs?.resolve && fs?.processPath
|
|
1446
|
+
? fs.processPath(await fs.resolve(scope.cwd, {})) : scope.cwd
|
|
1447
|
+
return { ok: true as const, ...updateJobs.start({ cwd, root: scope.root, path: rel, exe: scope.exe }) }
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
return { ok: false as const, error: '启动 SVN 更新失败:' + String(error) }
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
'svn.updatePoll': async (args: { sessionId?: string; jobId: string; since: number }) => {
|
|
1453
|
+
const scope = await updateScope(args.sessionId)
|
|
1454
|
+
if ('err' in scope) return { ok: false as const, error: scope.err }
|
|
1455
|
+
try { return { ok: true as const, ...updateJobs.poll(scope.root, args.jobId, args.since) } }
|
|
1456
|
+
catch (error) { return { ok: false as const, error: String(error) } }
|
|
1457
|
+
},
|
|
1458
|
+
'svn.updateCancel': async (args: { sessionId?: string; jobId: string }) => {
|
|
1459
|
+
const scope = await updateScope(args.sessionId)
|
|
1460
|
+
if ('err' in scope) return { ok: false as const, error: scope.err }
|
|
1461
|
+
try { return { ok: true as const, ...updateJobs.cancel(scope.root, args.jobId) } }
|
|
1462
|
+
catch (error) { return { ok: false as const, error: String(error) } }
|
|
1463
|
+
},
|
|
1464
|
+
'svn.updateActive': async (args: { sessionId?: string }) => {
|
|
1465
|
+
const scope = await updateScope(args.sessionId)
|
|
1466
|
+
if ('err' in scope) return { ok: false as const, error: scope.err }
|
|
1467
|
+
return { ok: true as const, job: updateJobs.active(scope.root) }
|
|
1468
|
+
},
|
|
1374
1469
|
'svn.tortoise': async (args: { sessionId?: string; action?: SvnAction; path?: string }) => {
|
|
1375
1470
|
const sc = await requireSvnSession(ctx, args.sessionId)
|
|
1376
1471
|
if ('err' in sc) return { ok: false as const, error: sc.err }
|
|
@@ -1397,5 +1492,5 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
1397
1492
|
}
|
|
1398
1493
|
},
|
|
1399
1494
|
}
|
|
1400
|
-
return { handlers }
|
|
1495
|
+
return { handlers, dispose: () => { updateJobs.dispose(); updateRootCache.clear() } }
|
|
1401
1496
|
}
|