@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/README.md +8 -0
- package/lib/client.js +618 -558
- package/lib/client.js.map +1 -1
- package/lib/index.js +47 -43
- package/package.json +1 -1
- package/src/client/icons.tsx +39 -0
- package/src/client/index.tsx +562 -576
- package/src/files.ts +14 -0
- package/src/index.ts +37 -34
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 {
|
|
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
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
189
|
+
launch('open', [isDirectory ? fullPath : '-R', fullPath])
|
|
187
190
|
} else {
|
|
188
|
-
|
|
191
|
+
launch('open', [fullPath])
|
|
189
192
|
}
|
|
190
193
|
} else {
|
|
191
194
|
if (action === 'reveal') {
|
|
192
|
-
|
|
195
|
+
launch('xdg-open', [isDirectory ? fullPath : resolve(fullPath, '..')])
|
|
193
196
|
} else {
|
|
194
|
-
|
|
197
|
+
launch('xdg-open', [fullPath])
|
|
195
198
|
}
|
|
196
199
|
}
|
|
197
200
|
|