dsh-vscode-mode 0.1.16 → 0.1.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 中央编辑器(文件页签/QuickOpen/状态栏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
@@ -101,3 +101,12 @@
101
101
  .vsm-compat-item.warn .vsm-compat-name::before{content:"● ";color:var(--dsw-alias-state-warn-primary,#e5b93f)}
102
102
  .vsm-compat-name{font-weight:500;min-width:0}
103
103
  .vsm-compat-note{color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:12px;text-align:right;min-width:0}
104
+
105
+ /* 开发形态开关 */
106
+ .vsm-devform-note{display:block;margin-top:4px;font-size:11px;color:var(--dsw-alias-label-tertiary,#9aa5b1);word-break:break-all}
107
+ .vsm-devform-actions{display:flex;flex-direction:column;align-items:flex-end;gap:6px;min-width:0}
108
+ .vsm-devform-msg{color:var(--dsw-alias-state-warn-primary,#e5b93f);font-size:11px;text-align:right}
109
+ .vsm-general-row button{border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;padding:6px 12px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;font-size:12px;white-space:nowrap}
110
+ .vsm-general-row button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
111
+ .vsm-general-row button:disabled{opacity:.55;cursor:default}
112
+ .vsm-general-row button.vsm-devform-close{color:var(--dsw-alias-state-error-primary,#f07878);border-color:var(--dsw-alias-state-error-secondary,#d9534f55)}
@@ -119,11 +119,14 @@ function ProjectGroup({ project, busy, onAdd, onRefresh, onToggle, onRemove }) {
119
119
  return React.createElement('section', { className: 'vsm-project' }, head, body)
120
120
  }
121
121
 
122
- /** 通用设置:选择当前对话文件链接的打开器。 */
122
+ /** 通用设置:选择当前对话文件链接的打开器 + 开发形态开关。 */
123
123
  function GeneralSettings({ registry }) {
124
124
  const [tool, setTool] = React.useState(AUTO_OPEN_TOOL)
125
125
  const [busy, setBusy] = React.useState(false)
126
126
  const [error, setError] = React.useState('')
127
+ const [devForm, setDevForm] = React.useState(null)
128
+ const [devBusy, setDevBusy] = React.useState(false)
129
+ const [devMessage, setDevMessage] = React.useState('')
127
130
  const settings = React.useContext(SettingsContext)
128
131
  const snapshot = settings?.getSnapshot?.()
129
132
  const loading = !snapshot || snapshot.status === 'loading'
@@ -137,19 +140,42 @@ function GeneralSettings({ registry }) {
137
140
  onChange()
138
141
  return settings?.subscribe?.(onChange)
139
142
  }, [settings])
143
+ React.useEffect(() => {
144
+ rpc('vscode.devFormGet', {}).then((result) => {
145
+ if (result.ok) setDevForm(result.devForm)
146
+ }).catch(() => { /* 开发形态查询失败静默 */ })
147
+ }, [])
140
148
  const save = (value) => {
141
149
  setTool(value); setBusy(true); setError('')
142
150
  if (!settings?.set) { setError('设置服务不可用'); setBusy(false); return }
143
151
  settings.set('fileOpenTool', value).catch((e) => setError(String(e))).finally(() => setBusy(false))
144
152
  }
153
+ const closeDevForm = () => {
154
+ if (!window.confirm('关闭开发形态:插件将切换为正式版安装(版本依赖 + 删除工作区链接),pnpm 装配后需重启 DSH 生效。确认关闭?')) return
155
+ setDevBusy(true)
156
+ setDevMessage('')
157
+ rpc('vscode.devFormSet', { enabled: false }).then((result) => {
158
+ if (!result.ok) { setDevMessage('关闭失败:' + String(result.error)); return }
159
+ setDevForm(result.devForm)
160
+ setDevMessage(result.restart ? '已切换为正式版,重启 DSH 后生效。' : '已关闭开发形态。')
161
+ }).catch((e) => setDevMessage('关闭失败:' + String(e))).finally(() => setDevBusy(false))
162
+ }
145
163
  const options = [{ id: AUTO_OPEN_TOOL, label: '自动选择', description: '按当前已注册工具自动选择' }, ...availableOpeners(registry).map((item) => ({ id: item.id, label: item.label, description: item.description }))]
146
164
  const currentOption = options.some((item) => item.id === tool) ? tool : AUTO_OPEN_TOOL
165
+ const devFormRow = devForm?.enabled ? React.createElement('div', { className: 'vsm-general-row' },
166
+ React.createElement('span', null, '开发形态', React.createElement('small', { className: 'vsm-devform-note' }, '工作区链接安装:' + devForm.path)),
167
+ React.createElement('div', { className: 'vsm-devform-actions' },
168
+ devMessage && React.createElement('small', { className: 'vsm-devform-msg' }, devMessage),
169
+ React.createElement('button', { className: 'vsm-devform-close', disabled: devBusy, onClick: closeDevForm }, devBusy ? '切换中…' : '关闭开发形态'),
170
+ ),
171
+ ) : null
147
172
  return React.createElement('section', { className: 'vsm-general-page' },
148
173
  React.createElement('h2', null, '通用'),
149
174
  React.createElement('p', null, '配置 DSH 对话中文件链接的打开工具。工具由当前运行时自动扫描。'),
150
175
  unavailable && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, '设置服务暂不可用,当前使用自动选择。'),
151
176
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
152
177
  React.createElement('label', { className: 'vsm-general-row' }, React.createElement('span', null, '文件链接使用工具'), React.createElement('select', { value: currentOption, disabled: loading || unavailable || notReady || busy || snapshot?.writable === false, onChange: (event) => save(event.target.value) }, options.map((item) => React.createElement('option', { key: item.id, value: item.id }, item.label))), loading && React.createElement('small', null, '读取中…')),
178
+ devFormRow,
153
179
  )
154
180
  }
155
181
 
@@ -171,10 +197,15 @@ function CompatSection({ getSummary }) {
171
197
  React.createElement('span', { className: 'vsm-compat-note' }, item.note ?? (item.active ? '正常' : '未生效')),
172
198
  ))
173
199
  const warnings = report?.warnings ?? []
200
+ const devForm = report?.devForm
174
201
  return React.createElement('section', { className: 'vsm-general-page' },
175
202
  React.createElement('h2', null, '兼容性'),
176
203
  React.createElement('p', null, '检测与其他插件 / DSH 版本的适配状态与已知问题。' + (report?.pluginVersion ? '(插件版本 ' + report.pluginVersion + ')' : '')),
177
204
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
205
+ devForm?.enabled && React.createElement(React.Fragment, null,
206
+ React.createElement('h3', { className: 'vsm-compat-h3' }, '开发形态'),
207
+ React.createElement('div', { className: 'vsm-compat-item ok' }, React.createElement('span', { className: 'vsm-compat-name' }, '开发形态开启(工作区链接安装)'), React.createElement('span', { className: 'vsm-compat-note' }, devForm.path)),
208
+ ),
178
209
  React.createElement('h3', { className: 'vsm-compat-h3' }, '外部插件与服务'),
179
210
  renderItems(report?.external),
180
211
  React.createElement('h3', { className: 'vsm-compat-h3' }, '客户端适配'),
package/src/compat.ts CHANGED
@@ -11,6 +11,7 @@ import { dirname, join } from 'node:path'
11
11
  import { fileURLToPath } from 'node:url'
12
12
  import { entriesOf } from './mcp.js'
13
13
  import { loadSettingsDeps } from './fileOpenSettings.js'
14
+ import { readDevForm } from './devForm.js'
14
15
  import type { CompatAdapter, CompatReport } from './shared/compat.js'
15
16
  import type { Ctx } from './store.js'
16
17
 
@@ -36,7 +37,7 @@ export function entryHash(id: string): string | undefined {
36
37
  }
37
38
 
38
39
  /** 读取随包 package.json 版本号(缺失/解析失败降级为空串)。 */
39
- function pluginVersionOf(): string {
40
+ export function pluginVersionOf(): string {
40
41
  try {
41
42
  const file = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')
42
43
  const pkg = JSON.parse(readFileSync(file, 'utf8')) as { version?: unknown }
@@ -142,5 +143,5 @@ export async function buildReport(
142
143
  for (const guard of guards) {
143
144
  if (!guard.active && guard.note) warnings.push(guard.note)
144
145
  }
145
- return { pluginVersion: options?.version ?? pluginVersionOf(), external, guards, warnings }
146
+ return { pluginVersion: options?.version ?? pluginVersionOf(), external, guards, warnings, devForm: readDevForm() }
146
147
  }
package/src/devForm.ts ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * dsh-vscode-mode host — 开发形态管理(安装形态切换)。
3
+ * 开发形态 = profile 中以 link: 依赖 + junction 指向工作区的安装(用于插件开发);
4
+ * 关闭 = 改回版本依赖 + 删 junction + pnpm install 装配正式版。
5
+ * 切换后需重启 DSH 生效(运行中的代码仍是旧形态)。
6
+ * 与 compat.ts 存在模块环(devForm 用 pluginVersionOf、compat 用 readDevForm),
7
+ * 双方仅在函数体内交叉使用,模块求值期无依赖,ESM 安全。
8
+ * 作者 ddj 2026-08-24
9
+ */
10
+ import { existsSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
11
+ import { dirname, join } from 'node:path'
12
+ import { homedir } from 'node:os'
13
+ import { PLUGIN_NAME, pluginVersionOf } from './compat.js'
14
+ import type { DevFormInfo } from './shared/compat.js'
15
+ import type { Ctx } from './store.js'
16
+
17
+ /** 扫描 $DSH_HOME/profiles 找到依赖本插件的 profile 目录(无 env 依赖,多 profile 安全)。 */
18
+ export function findProfileDir(): string | undefined {
19
+ const home = process.env.DSH_HOME ?? join(homedir(), '.dsh')
20
+ try {
21
+ for (const name of readdirSync(join(home, 'profiles'))) {
22
+ const pkgFile = join(home, 'profiles', name, 'package.json')
23
+ if (!existsSync(pkgFile)) continue
24
+ try {
25
+ const pkg = JSON.parse(readFileSync(pkgFile, 'utf8')) as { dependencies?: Record<string, unknown> } | undefined
26
+ if (pkg?.dependencies?.[PLUGIN_NAME]) return dirname(pkgFile)
27
+ } catch {
28
+ /* 坏 manifest 跳过该 profile */
29
+ }
30
+ }
31
+ } catch {
32
+ /* profiles 目录不存在时放弃 */
33
+ }
34
+ return undefined
35
+ }
36
+
37
+ /**
38
+ * 读取当前开发形态(manifest link: 判定)。
39
+ * @author ddj 2026年08月24号
40
+ * @returns 开发形态状态
41
+ */
42
+ export function readDevForm(): DevFormInfo {
43
+ const profileDir = findProfileDir()
44
+ if (!profileDir) return { enabled: false }
45
+ try {
46
+ const text = readFileSync(join(profileDir, 'package.json'), 'utf8').replace(/^\uFEFF/, '')
47
+ const pkg = JSON.parse(text) as { dependencies?: Record<string, unknown> } | undefined
48
+ const spec = pkg?.dependencies?.[PLUGIN_NAME]
49
+ if (typeof spec === 'string' && spec.startsWith('link:')) {
50
+ return { enabled: true, path: spec.slice('link:'.length) }
51
+ }
52
+ } catch {
53
+ /* manifest 不可读视为非开发形态 */
54
+ }
55
+ return { enabled: false }
56
+ }
57
+
58
+ /**
59
+ * 规划切换后的 manifest 文本(纯函数,可单测;非法 JSON 抛错)。
60
+ * @author ddj 2026年08月24号
61
+ * @param text 原 manifest 文本
62
+ * @param enabled 目标形态(true=link:,false=版本)
63
+ * @param path 工作区路径(开启时必填)
64
+ * @param version 版本号(关闭时使用,写入 ^<version>)
65
+ * @returns 新 manifest 文本(无 BOM,2 空格缩进)
66
+ */
67
+ export function planManifest(text: string, enabled: boolean, path: string, version: string): string {
68
+ const data = JSON.parse(text.replace(/^\uFEFF/, '')) as { dependencies?: Record<string, unknown> }
69
+ if (!data.dependencies || typeof data.dependencies !== 'object') {
70
+ throw new Error('profile package.json 缺少 dependencies 字段')
71
+ }
72
+ data.dependencies[PLUGIN_NAME] = enabled ? 'link:' + path.replace(/\\/g, '/') : '^' + version
73
+ return JSON.stringify(data, null, 2) + '\n'
74
+ }
75
+
76
+ /** 建立或移除 node_modules 下的开发 junction(目标不存在时开启失败)。 */
77
+ function applyJunction(profileDir: string, enabled: boolean, target: string): void {
78
+ const linkDir = join(profileDir, 'node_modules', PLUGIN_NAME)
79
+ if (existsSync(linkDir)) rmSync(linkDir, { recursive: true, force: true })
80
+ if (!enabled) return
81
+ if (!existsSync(target)) throw new Error('工作区路径不存在:' + target)
82
+ symlinkSync(target, linkDir, 'junction')
83
+ }
84
+
85
+ /**
86
+ * 切换开发形态:改 manifest → 建/删 junction → pnpm install(subprocess)。
87
+ * @author ddj 2026年08月24号
88
+ * @param ctx DSH host 上下文(subprocess 服务)
89
+ * @param enabled 目标形态
90
+ * @param path 工作区绝对路径(开启时必填)
91
+ * @returns 切换结果(restart=true 表示需重启 DSH 生效)
92
+ */
93
+ export async function setDevForm(ctx: Ctx, enabled: boolean, path?: string): Promise<{ ok: boolean; error?: string; restart: boolean }> {
94
+ const profileDir = findProfileDir()
95
+ if (!profileDir) return { ok: false, error: '未找到依赖本插件的 profile(检查 DSH_HOME/profiles)', restart: false }
96
+ const current = readDevForm()
97
+ const target = (path ?? current.path ?? '').replace(/\\/g, '/')
98
+ if (enabled && !target) return { ok: false, error: '开启开发形态需要提供工作区路径(path 参数)', restart: false }
99
+ if (enabled === current.enabled && (!enabled || target === current.path)) {
100
+ return { ok: true, restart: false }
101
+ }
102
+ try {
103
+ const pkgFile = join(profileDir, 'package.json')
104
+ const version = pluginVersionOf()
105
+ if (!version) return { ok: false, error: '读取插件版本失败,无法写入版本依赖', restart: false }
106
+ const next = planManifest(readFileSync(pkgFile, 'utf8').replace(/^\uFEFF/, ''), enabled, target, version)
107
+ writeFileSync(pkgFile, next, 'utf8')
108
+ applyJunction(profileDir, enabled, target)
109
+ const installError = await runPnpmInstall(ctx, profileDir)
110
+ return { ok: !installError, error: installError, restart: true }
111
+ } catch (error) {
112
+ return { ok: false, error: String(error), restart: false }
113
+ }
114
+ }
115
+
116
+ /** 在 profile 目录执行 pnpm install(subprocess 契约:spawn({argv,cwd,stdio,graceMs}))。 */
117
+ async function runPnpmInstall(ctx: Ctx, profileDir: string): Promise<string | undefined> {
118
+ const sub = ctx.get('subprocess')
119
+ if (!sub || typeof sub.spawn !== 'function') return 'manifest/junction 已更新,但缺少 subprocess:请手动在 profile 目录运行 pnpm install'
120
+ const handle = sub.spawn({
121
+ argv: ['pnpm', 'install'],
122
+ cwd: profileDir,
123
+ stdio: { stdout: { maxBytes: 1 << 16 }, stderr: { maxBytes: 1 << 16 }, stdin: 'ignore' },
124
+ graceMs: 120000,
125
+ })
126
+ const outcome = await handle.done
127
+ const code = outcome?.exitCode ?? outcome?.code
128
+ if (code !== 0) {
129
+ const stderr = handle.collected?.stderr?.readFrom(0).text ?? ''
130
+ return 'pnpm install 失败(exit ' + code + '):' + stderr.slice(0, 500)
131
+ }
132
+ return undefined
133
+ }
package/src/rpc.ts CHANGED
@@ -28,6 +28,7 @@ import { listMcp, refreshMcp, removeMcp, saveMcp, toggleMcp } from './mcp.js'
28
28
  import { listProjects, projectRefresh, projectRemove, projectSave, projectToggle } from './mcpProject.js'
29
29
  import { normalizeFileOpenTool, FILE_OPEN_DEFAULT, FILE_OPEN_SETTINGS_NS } from './fileOpenSettings.js'
30
30
  import { buildReport } from './compat.js'
31
+ import { readDevForm, setDevForm } from './devForm.js'
31
32
 
32
33
  /** cwd → Promise 链:串行化 debug 日志追加(fs read+write 非原子,避免并发丢行)。 */
33
34
  const debugWriteQueues = new Map<string, Promise<void>>()
@@ -324,6 +325,16 @@ export function buildHandlers(ctx: Ctx, registry: Registry, searcher = newSearch
324
325
  }
325
326
  },
326
327
  'compat': async () => ({ ok: true, report: await buildReport(ctx) }),
328
+ 'vscode.devFormGet': async () => ({ ok: true, devForm: readDevForm() }),
329
+ 'vscode.devFormSet': async (args) => {
330
+ try {
331
+ const result = await setDevForm(ctx, args.enabled === true, args.path)
332
+ if (!result.ok) return { ok: false, error: result.error ?? '切换开发形态失败' }
333
+ return { ok: true, devForm: readDevForm(), restart: result.restart }
334
+ } catch (error) {
335
+ return { ok: false, error: String(error) }
336
+ }
337
+ },
327
338
  }
328
339
  }
329
340
 
@@ -11,10 +11,17 @@ export interface CompatAdapter {
11
11
  note?: string
12
12
  }
13
13
 
14
+ /** 开发形态信息:profile 中以 link: 依赖 + junction 指向工作区的安装形态。 */
15
+ export interface DevFormInfo {
16
+ enabled: boolean
17
+ path?: string
18
+ }
19
+
14
20
  /** 兼容性报告:host 侧构建(RPC edrv.compat 返回),client 侧补充本地适配项后展示。 */
15
21
  export interface CompatReport {
16
22
  pluginVersion: string
17
23
  external: CompatAdapter[]
18
24
  guards: CompatAdapter[]
19
25
  warnings: string[]
26
+ devForm?: DevFormInfo
20
27
  }
package/src/shared/rpc.ts CHANGED
@@ -13,7 +13,7 @@ import type {
13
13
  StaleHunk,
14
14
  } from './types.js'
15
15
  import type { MpcConfig, MpcProject, MpcProjectSaveInput, MpcServer } from './mcp.js'
16
- import type { CompatReport } from './compat.js'
16
+ import type { CompatReport, DevFormInfo } from './compat.js'
17
17
 
18
18
  /** webServer 精确路由。 */
19
19
  export const RPC_PATH = '/edrv/rpc'
@@ -46,6 +46,8 @@ export interface RpcRequestMap {
46
46
  'mcp.projectRefresh': { workspacePath: string; serverName: string }
47
47
  'vscode.fileOpenSettingsGet': {}
48
48
  'vscode.fileOpenSettingsUpdate': { fileOpenTool: string; expectedRevision?: number }
49
+ 'vscode.devFormGet': {}
50
+ 'vscode.devFormSet': { enabled: boolean; path?: string }
49
51
  'compat': {}
50
52
  }
51
53
 
@@ -76,6 +78,8 @@ export interface RpcOkMap {
76
78
  'mcp.projectRefresh': { project: MpcProject }
77
79
  'vscode.fileOpenSettingsGet': { fileOpenTool: string; revision?: number }
78
80
  'vscode.fileOpenSettingsUpdate': { fileOpenTool: string; revision?: number }
81
+ 'vscode.devFormGet': { devForm: DevFormInfo }
82
+ 'vscode.devFormSet': { devForm: DevFormInfo; restart: boolean }
79
83
  'compat': { report: CompatReport }
80
84
  }
81
85