@civilization/dsh-workspace-files 0.3.1 → 0.3.3

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/src/files.ts CHANGED
@@ -48,6 +48,20 @@ const DEFAULT_IGNORED_DIRS = new Set([
48
48
  'temp',
49
49
  'logs',
50
50
  '.dsh',
51
+ '.yarn',
52
+ '.vite',
53
+ '.parcel-cache',
54
+ '.rollup.cache',
55
+ '.tsdown',
56
+ '.svelte-kit',
57
+ '.astro',
58
+ '.mypy_cache',
59
+ '.pytest_cache',
60
+ '.ruff_cache',
61
+ 'bin',
62
+ 'obj',
63
+ '.dart_tool',
64
+ '.gemini',
51
65
  ])
52
66
 
53
67
  function isWithin(parent: string, child: string): boolean {
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { exec, spawn } from 'node:child_process'
2
- import { isAbsolute, resolve } from 'node:path'
1
+ import { spawn } from 'node:child_process'
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path'
3
3
  import type { Context } from '@deepseek-ai/cordis'
4
4
  import Schema from '@deepseek-ai/schemastery'
5
5
  import { scanWorkspaceTree } from './files.js'
@@ -79,6 +79,30 @@ function writeJson(res: any, status: number, body: any): void {
79
79
  res.end(data)
80
80
  }
81
81
 
82
+ /**
83
+ * 拒绝工作区之外的目标:files.open 的 path 来自请求体,必须是工作区内的相对/绝对路径,
84
+ * 否则模型或页面脚本可以借宿主进程去打开任意本地路径。
85
+ */
86
+ function assertInsideWorkspace(cwd: string, fullPath: string): void {
87
+ const root = resolve(cwd)
88
+ const target = resolve(fullPath)
89
+ const rel = relative(root, target)
90
+ const escaped = rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)
91
+ if (escaped) throw new Error('Path must stay inside the workspace.')
92
+ }
93
+
94
+ /**
95
+ * 启动系统文件管理器/默认应用。spawn 在命令不存在时会 emit 'error',
96
+ * 没有监听器就会变成未捕获异常拖垮宿主进程,因此这里必须兜住。
97
+ */
98
+ function launch(command: string, args: string[]): void {
99
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' })
100
+ child.on('error', (err: any) => {
101
+ console.warn(`[dsh-workspace-files] failed to launch ${command}:`, err?.message || String(err))
102
+ })
103
+ child.unref()
104
+ }
105
+
82
106
  function resolveCwd(ctx: Context, body: Record<string, any>): string {
83
107
  if (body.cwd?.trim()) return body.cwd.trim()
84
108
  if (body.root?.trim()) return body.root.trim()
@@ -107,25 +131,9 @@ export function apply(ctx: Context, config: Config = {}): void {
107
131
  showHidden: Boolean(config?.showHidden),
108
132
  }
109
133
 
110
- // 关键:向 Host 的 settings 服务注册 settings namespace,使设置面板的前端控制器能够枚举到此插件卡片
111
- if ((ctx as any).inject) {
112
- (ctx as any).inject(['settings'], (scopedCtx: any) => {
113
- try {
114
- const scope = scopedCtx.settings?.register(SETTINGS_NS, Config, {
115
- base: liveConfig,
116
- })
117
- const applyChange = () => {
118
- const current = scope?.get?.()
119
- if (typeof current?.maxDepth === 'number') liveConfig.maxDepth = current.maxDepth
120
- if (typeof current?.showHidden === 'boolean') liveConfig.showHidden = current.showHidden
121
- }
122
- applyChange()
123
- scope?.watch?.(applyChange)
124
- } catch (err) {
125
- console.warn('[dsh-workspace-files] failed to register settings scope:', err)
126
- }
127
- })
128
- }
134
+ // 设置表单由 DSH 从本插件导出的 Config schema 自动生成(Host 侧 settings.describe()),
135
+ // 无需也不存在插件侧的 settings.register():DSH 的 settings 服务只提供
136
+ // describe/update/replace/mutate/configure,namespace 就是 profile 里的 entry id。
129
137
 
130
138
  const webServer = (ctx as any).webServer
131
139
  if (!webServer) return
@@ -167,31 +175,26 @@ export function apply(ctx: Context, config: Config = {}): void {
167
175
  }
168
176
 
169
177
  const fullPath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath)
178
+ assertInsideWorkspace(cwd, fullPath)
170
179
  const action = body.action || 'reveal'
171
180
  const isDirectory = Boolean(body.isDirectory)
172
181
 
173
182
  if (process.platform === 'win32') {
174
183
  const winPath = fullPath.replace(/\//g, '\\')
175
- if (action === 'reveal') {
176
- if (isDirectory) {
177
- spawn('explorer.exe', [winPath], { detached: true, stdio: 'ignore' }).unref()
178
- } else {
179
- spawn('explorer.exe', [`/select,${winPath}`], { detached: true, stdio: 'ignore' }).unref()
180
- }
181
- } else {
182
- exec(`start "" "${winPath.replace(/"/g, '\\"')}"`, { windowsHide: true })
183
- }
184
+ // explorer.exe 自己解析路径,不再经过 cmd.exe 的字符串拼接,避免 & | ^ 等注入
185
+ const args = action === 'reveal' && !isDirectory ? [`/select,${winPath}`] : [winPath]
186
+ launch('explorer.exe', args)
184
187
  } else if (process.platform === 'darwin') {
185
188
  if (action === 'reveal') {
186
- spawn('open', [isDirectory ? fullPath : '-R', fullPath], { detached: true, stdio: 'ignore' }).unref()
189
+ launch('open', [isDirectory ? fullPath : '-R', fullPath])
187
190
  } else {
188
- spawn('open', [fullPath], { detached: true, stdio: 'ignore' }).unref()
191
+ launch('open', [fullPath])
189
192
  }
190
193
  } else {
191
194
  if (action === 'reveal') {
192
- spawn('xdg-open', [isDirectory ? fullPath : resolve(fullPath, '..')], { detached: true, stdio: 'ignore' }).unref()
195
+ launch('xdg-open', [isDirectory ? fullPath : resolve(fullPath, '..')])
193
196
  } else {
194
- spawn('xdg-open', [fullPath], { detached: true, stdio: 'ignore' }).unref()
197
+ launch('xdg-open', [fullPath])
195
198
  }
196
199
  }
197
200