@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/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import Schema from "@deepseek-ai/schemastery";
|
|
4
4
|
import { promises } from "node:fs";
|
|
5
5
|
//#region src/files.ts
|
|
@@ -32,7 +32,21 @@ const DEFAULT_IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
32
32
|
"tmp",
|
|
33
33
|
"temp",
|
|
34
34
|
"logs",
|
|
35
|
-
".dsh"
|
|
35
|
+
".dsh",
|
|
36
|
+
".yarn",
|
|
37
|
+
".vite",
|
|
38
|
+
".parcel-cache",
|
|
39
|
+
".rollup.cache",
|
|
40
|
+
".tsdown",
|
|
41
|
+
".svelte-kit",
|
|
42
|
+
".astro",
|
|
43
|
+
".mypy_cache",
|
|
44
|
+
".pytest_cache",
|
|
45
|
+
".ruff_cache",
|
|
46
|
+
"bin",
|
|
47
|
+
"obj",
|
|
48
|
+
".dart_tool",
|
|
49
|
+
".gemini"
|
|
36
50
|
]);
|
|
37
51
|
function isWithin(parent, child) {
|
|
38
52
|
const rel = relative(parent, child);
|
|
@@ -181,6 +195,30 @@ function writeJson(res, status, body) {
|
|
|
181
195
|
});
|
|
182
196
|
res.end(data);
|
|
183
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* 拒绝工作区之外的目标:files.open 的 path 来自请求体,必须是工作区内的相对/绝对路径,
|
|
200
|
+
* 否则模型或页面脚本可以借宿主进程去打开任意本地路径。
|
|
201
|
+
*/
|
|
202
|
+
function assertInsideWorkspace(cwd, fullPath) {
|
|
203
|
+
const root = resolve(cwd);
|
|
204
|
+
const target = resolve(fullPath);
|
|
205
|
+
const rel = relative(root, target);
|
|
206
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error("Path must stay inside the workspace.");
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* 启动系统文件管理器/默认应用。spawn 在命令不存在时会 emit 'error',
|
|
210
|
+
* 没有监听器就会变成未捕获异常拖垮宿主进程,因此这里必须兜住。
|
|
211
|
+
*/
|
|
212
|
+
function launch(command, args) {
|
|
213
|
+
const child = spawn(command, args, {
|
|
214
|
+
detached: true,
|
|
215
|
+
stdio: "ignore"
|
|
216
|
+
});
|
|
217
|
+
child.on("error", (err) => {
|
|
218
|
+
console.warn(`[dsh-workspace-files] failed to launch ${command}:`, err?.message || String(err));
|
|
219
|
+
});
|
|
220
|
+
child.unref();
|
|
221
|
+
}
|
|
184
222
|
function resolveCwd(ctx, body) {
|
|
185
223
|
if (body.cwd?.trim()) return body.cwd.trim();
|
|
186
224
|
if (body.root?.trim()) return body.root.trim();
|
|
@@ -198,20 +236,6 @@ function apply(ctx, config = {}) {
|
|
|
198
236
|
maxDepth: typeof config?.maxDepth === "number" && config.maxDepth > 0 ? config.maxDepth : 16,
|
|
199
237
|
showHidden: Boolean(config?.showHidden)
|
|
200
238
|
};
|
|
201
|
-
if (ctx.inject) ctx.inject(["settings"], (scopedCtx) => {
|
|
202
|
-
try {
|
|
203
|
-
const scope = scopedCtx.settings?.register(SETTINGS_NS, Config, { base: liveConfig });
|
|
204
|
-
const applyChange = () => {
|
|
205
|
-
const current = scope?.get?.();
|
|
206
|
-
if (typeof current?.maxDepth === "number") liveConfig.maxDepth = current.maxDepth;
|
|
207
|
-
if (typeof current?.showHidden === "boolean") liveConfig.showHidden = current.showHidden;
|
|
208
|
-
};
|
|
209
|
-
applyChange();
|
|
210
|
-
scope?.watch?.(applyChange);
|
|
211
|
-
} catch (err) {
|
|
212
|
-
console.warn("[dsh-workspace-files] failed to register settings scope:", err);
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
239
|
const webServer = ctx.webServer;
|
|
216
240
|
if (!webServer) return;
|
|
217
241
|
const handleRpc = async (req, res, prefix) => {
|
|
@@ -261,37 +285,17 @@ function apply(ctx, config = {}) {
|
|
|
261
285
|
return;
|
|
262
286
|
}
|
|
263
287
|
const fullPath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
288
|
+
assertInsideWorkspace(cwd, fullPath);
|
|
264
289
|
const action = body.action || "reveal";
|
|
265
290
|
const isDirectory = Boolean(body.isDirectory);
|
|
266
291
|
if (process.platform === "win32") {
|
|
267
292
|
const winPath = fullPath.replace(/\//g, "\\");
|
|
268
|
-
|
|
269
|
-
if (isDirectory) spawn("explorer.exe", [winPath], {
|
|
270
|
-
detached: true,
|
|
271
|
-
stdio: "ignore"
|
|
272
|
-
}).unref();
|
|
273
|
-
else spawn("explorer.exe", [`/select,${winPath}`], {
|
|
274
|
-
detached: true,
|
|
275
|
-
stdio: "ignore"
|
|
276
|
-
}).unref();
|
|
277
|
-
} else exec(`start "" "${winPath.replace(/"/g, "\\\"")}"`, { windowsHide: true });
|
|
293
|
+
launch("explorer.exe", action === "reveal" && !isDirectory ? [`/select,${winPath}`] : [winPath]);
|
|
278
294
|
} else if (process.platform === "darwin") {
|
|
279
|
-
if (action === "reveal")
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
else spawn("open", [fullPath], {
|
|
284
|
-
detached: true,
|
|
285
|
-
stdio: "ignore"
|
|
286
|
-
}).unref();
|
|
287
|
-
} else if (action === "reveal") spawn("xdg-open", [isDirectory ? fullPath : resolve(fullPath, "..")], {
|
|
288
|
-
detached: true,
|
|
289
|
-
stdio: "ignore"
|
|
290
|
-
}).unref();
|
|
291
|
-
else spawn("xdg-open", [fullPath], {
|
|
292
|
-
detached: true,
|
|
293
|
-
stdio: "ignore"
|
|
294
|
-
}).unref();
|
|
295
|
+
if (action === "reveal") launch("open", [isDirectory ? fullPath : "-R", fullPath]);
|
|
296
|
+
else launch("open", [fullPath]);
|
|
297
|
+
} else if (action === "reveal") launch("xdg-open", [isDirectory ? fullPath : resolve(fullPath, "..")]);
|
|
298
|
+
else launch("xdg-open", [fullPath]);
|
|
295
299
|
writeJson(res, 200, { ok: true });
|
|
296
300
|
} catch (err) {
|
|
297
301
|
writeJson(res, 500, {
|
package/package.json
CHANGED
package/src/client/icons.tsx
CHANGED
|
@@ -120,6 +120,30 @@ export function FileGlyphIcon({ filename, size = 14 }: { filename: string; size?
|
|
|
120
120
|
} else if (ext === 'yml' || ext === 'yaml') {
|
|
121
121
|
color = '#cb171e'
|
|
122
122
|
badge = 'Y'
|
|
123
|
+
} else if (ext === 'rs') {
|
|
124
|
+
color = '#dea584'
|
|
125
|
+
badge = 'RS'
|
|
126
|
+
} else if (ext === 'go') {
|
|
127
|
+
color = '#00add8'
|
|
128
|
+
badge = 'GO'
|
|
129
|
+
} else if (ext === 'vue') {
|
|
130
|
+
color = '#41b883'
|
|
131
|
+
badge = 'V'
|
|
132
|
+
} else if (ext === 'svelte') {
|
|
133
|
+
color = '#ff3e00'
|
|
134
|
+
badge = 'S'
|
|
135
|
+
} else if (ext === 'toml') {
|
|
136
|
+
color = '#9c4121'
|
|
137
|
+
badge = 'T'
|
|
138
|
+
} else if (ext === 'sql') {
|
|
139
|
+
color = '#e38c00'
|
|
140
|
+
badge = 'SQL'
|
|
141
|
+
} else if (ext === 'sh' || ext === 'bash' || ext === 'zsh' || ext === 'ps1' || ext === 'bat' || ext === 'cmd') {
|
|
142
|
+
color = '#4eaa25'
|
|
143
|
+
badge = '>_'
|
|
144
|
+
} else if (ext === 'lock') {
|
|
145
|
+
color = 'var(--dsw-alias-label-tertiary, #8c9ba5)'
|
|
146
|
+
badge = '🔒'
|
|
123
147
|
}
|
|
124
148
|
|
|
125
149
|
return (
|
|
@@ -159,6 +183,21 @@ export function IconTree({ size = 13 }: { size?: number }): JSX.Element {
|
|
|
159
183
|
)
|
|
160
184
|
}
|
|
161
185
|
|
|
186
|
+
export function IconChevronRight({ size = 11, className, style }: { size?: number; className?: string; style?: React.CSSProperties }): JSX.Element {
|
|
187
|
+
return (
|
|
188
|
+
<svg
|
|
189
|
+
width={size}
|
|
190
|
+
height={size}
|
|
191
|
+
viewBox="0 0 16 16"
|
|
192
|
+
fill="currentColor"
|
|
193
|
+
className={className}
|
|
194
|
+
style={{ display: 'inline-block', verticalAlign: 'middle', flexShrink: 0, ...style }}
|
|
195
|
+
>
|
|
196
|
+
<path fillRule="evenodd" d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06z" />
|
|
197
|
+
</svg>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
162
201
|
export function IconChevronDown({ size = 14 }: { size?: number }): JSX.Element {
|
|
163
202
|
return (
|
|
164
203
|
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'inline-block', verticalAlign: 'middle', flexShrink: 0 }}>
|