@civilization/dsh-workspace-files 0.2.0 → 0.3.1
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 +22 -1
- package/lib/client.js +655 -60
- package/lib/client.js.map +1 -1
- package/lib/index.js +198 -36
- package/package.json +8 -2
- package/src/client/icons.tsx +27 -0
- package/src/client/index.tsx +587 -8
- package/src/files.ts +75 -34
- package/src/index.ts +132 -4
package/lib/index.js
CHANGED
|
@@ -1,82 +1,119 @@
|
|
|
1
|
+
import { exec, spawn } from "node:child_process";
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
1
4
|
import { promises } from "node:fs";
|
|
2
|
-
import { join, relative, resolve } from "node:path";
|
|
3
5
|
//#region src/files.ts
|
|
4
6
|
const DEFAULT_IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
5
7
|
".git",
|
|
6
8
|
".svn",
|
|
7
9
|
".hg",
|
|
8
10
|
"node_modules",
|
|
11
|
+
".pnpm-store",
|
|
9
12
|
".turbo",
|
|
10
13
|
".next",
|
|
14
|
+
".nuxt",
|
|
15
|
+
".output",
|
|
11
16
|
"dist",
|
|
12
17
|
"build",
|
|
13
|
-
"
|
|
18
|
+
"out",
|
|
19
|
+
".cache",
|
|
20
|
+
".idea",
|
|
21
|
+
".vscode",
|
|
22
|
+
".husky",
|
|
23
|
+
"venv",
|
|
24
|
+
".venv",
|
|
25
|
+
"env",
|
|
26
|
+
"__pycache__",
|
|
27
|
+
"target",
|
|
28
|
+
".gradle",
|
|
29
|
+
".cargo",
|
|
30
|
+
"vendor",
|
|
31
|
+
"coverage",
|
|
32
|
+
"tmp",
|
|
33
|
+
"temp",
|
|
34
|
+
"logs",
|
|
35
|
+
".dsh"
|
|
14
36
|
]);
|
|
15
37
|
function isWithin(parent, child) {
|
|
16
38
|
const rel = relative(parent, child);
|
|
17
39
|
return !rel.startsWith("..") && !rel.includes(":");
|
|
18
40
|
}
|
|
41
|
+
async function loadGitIgnoreNames(rootDir) {
|
|
42
|
+
const names = /* @__PURE__ */ new Set();
|
|
43
|
+
try {
|
|
44
|
+
const gitignorePath = join(rootDir, ".gitignore");
|
|
45
|
+
const content = await promises.readFile(gitignorePath, "utf8");
|
|
46
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
47
|
+
const line = rawLine.trim();
|
|
48
|
+
if (!line || line.startsWith("#") || line.startsWith("!")) continue;
|
|
49
|
+
const clean = line.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
50
|
+
if (clean && !clean.includes("*") && !clean.includes("?") && !clean.includes("/")) names.add(clean);
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
return names;
|
|
54
|
+
}
|
|
19
55
|
async function scanWorkspaceTree(cwd, options = {}) {
|
|
20
56
|
const root = resolve(cwd);
|
|
21
|
-
const maxDepth = options.maxDepth ??
|
|
57
|
+
const maxDepth = options.maxDepth ?? 16;
|
|
22
58
|
const showHidden = options.showHidden ?? false;
|
|
23
59
|
const showIgnored = options.showIgnored ?? false;
|
|
60
|
+
const gitIgnoredNames = showIgnored ? /* @__PURE__ */ new Set() : await loadGitIgnoreNames(root);
|
|
24
61
|
let totalFiles = 0;
|
|
25
62
|
let totalDirs = 0;
|
|
63
|
+
let totalNodes = 0;
|
|
64
|
+
const MAX_NODES_SAFETY_LIMIT = 3e4;
|
|
26
65
|
async function walk(dirPath, currentDepth) {
|
|
27
|
-
if (currentDepth > maxDepth) return [];
|
|
66
|
+
if (currentDepth > maxDepth || totalNodes >= MAX_NODES_SAFETY_LIMIT) return [];
|
|
28
67
|
let dirents;
|
|
29
68
|
try {
|
|
30
69
|
dirents = await promises.readdir(dirPath, { withFileTypes: true });
|
|
31
70
|
} catch {
|
|
32
71
|
return [];
|
|
33
72
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
37
|
-
return a.name.localeCompare(b.name, void 0, {
|
|
38
|
-
numeric: true,
|
|
39
|
-
sensitivity: "base"
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
const items = [];
|
|
73
|
+
const dirTasks = [];
|
|
74
|
+
const fileItems = [];
|
|
43
75
|
for (const ent of dirents) {
|
|
76
|
+
if (totalNodes >= MAX_NODES_SAFETY_LIMIT) break;
|
|
44
77
|
const name = ent.name;
|
|
45
78
|
if (!showHidden && name.startsWith(".") && name !== ".gitignore" && name !== ".env") {
|
|
46
79
|
if (!showIgnored) continue;
|
|
47
80
|
}
|
|
48
|
-
if (!showIgnored
|
|
81
|
+
if (!showIgnored) {
|
|
82
|
+
if (ent.isDirectory() && DEFAULT_IGNORED_DIRS.has(name)) continue;
|
|
83
|
+
if (gitIgnoredNames.has(name)) continue;
|
|
84
|
+
}
|
|
49
85
|
const fullPath = join(dirPath, name);
|
|
50
86
|
if (!isWithin(root, fullPath)) continue;
|
|
51
87
|
const relPath = relative(root, fullPath).replace(/\\/g, "/");
|
|
52
88
|
if (ent.isDirectory()) {
|
|
53
89
|
totalDirs += 1;
|
|
54
|
-
|
|
55
|
-
|
|
90
|
+
totalNodes += 1;
|
|
91
|
+
dirTasks.push(walk(fullPath, currentDepth + 1).then((children) => ({
|
|
56
92
|
name,
|
|
57
93
|
path: relPath,
|
|
58
94
|
isDirectory: true,
|
|
59
95
|
children
|
|
60
|
-
});
|
|
96
|
+
})));
|
|
61
97
|
} else {
|
|
62
98
|
totalFiles += 1;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const st = await promises.stat(fullPath);
|
|
67
|
-
size = st.size;
|
|
68
|
-
mtime = st.mtimeMs;
|
|
69
|
-
} catch {}
|
|
70
|
-
items.push({
|
|
99
|
+
totalNodes += 1;
|
|
100
|
+
fileItems.push({
|
|
71
101
|
name,
|
|
72
102
|
path: relPath,
|
|
73
|
-
isDirectory: false
|
|
74
|
-
size,
|
|
75
|
-
mtime
|
|
103
|
+
isDirectory: false
|
|
76
104
|
});
|
|
77
105
|
}
|
|
78
106
|
}
|
|
79
|
-
|
|
107
|
+
const validDirs = (await Promise.all(dirTasks)).filter((d) => d !== null);
|
|
108
|
+
validDirs.sort((a, b) => a.name.localeCompare(b.name, void 0, {
|
|
109
|
+
numeric: true,
|
|
110
|
+
sensitivity: "base"
|
|
111
|
+
}));
|
|
112
|
+
fileItems.sort((a, b) => a.name.localeCompare(b.name, void 0, {
|
|
113
|
+
numeric: true,
|
|
114
|
+
sensitivity: "base"
|
|
115
|
+
}));
|
|
116
|
+
return [...validDirs, ...fileItems];
|
|
80
117
|
}
|
|
81
118
|
return {
|
|
82
119
|
root,
|
|
@@ -89,6 +126,10 @@ async function scanWorkspaceTree(cwd, options = {}) {
|
|
|
89
126
|
//#region src/index.ts
|
|
90
127
|
const name = "@civilization/dsh-workspace-files";
|
|
91
128
|
const inject = ["webServer"];
|
|
129
|
+
const Config = Schema.object({
|
|
130
|
+
maxDepth: Schema.natural().min(1).max(100).default(16).description("工作区文件树扫描的最大文件夹深度(层级)"),
|
|
131
|
+
showHidden: Schema.boolean().default(false).description("默认是否显示以点开头的隐藏文件")
|
|
132
|
+
}).description("工作区文件:浏览与检索工作区文件树");
|
|
92
133
|
async function readJsonBody(req) {
|
|
93
134
|
if (req.body && typeof req.body === "object") return req.body;
|
|
94
135
|
if (typeof req.body === "string" && req.body.trim()) try {
|
|
@@ -149,7 +190,28 @@ function resolveCwd(ctx, body) {
|
|
|
149
190
|
} catch {}
|
|
150
191
|
return process.cwd();
|
|
151
192
|
}
|
|
152
|
-
|
|
193
|
+
const SETTINGS_NS = "workspace-files";
|
|
194
|
+
const scanCache = /* @__PURE__ */ new Map();
|
|
195
|
+
const CACHE_TTL_MS = 2e4;
|
|
196
|
+
function apply(ctx, config = {}) {
|
|
197
|
+
const liveConfig = {
|
|
198
|
+
maxDepth: typeof config?.maxDepth === "number" && config.maxDepth > 0 ? config.maxDepth : 16,
|
|
199
|
+
showHidden: Boolean(config?.showHidden)
|
|
200
|
+
};
|
|
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
|
+
});
|
|
153
215
|
const webServer = ctx.webServer;
|
|
154
216
|
if (!webServer) return;
|
|
155
217
|
const handleRpc = async (req, res, prefix) => {
|
|
@@ -162,6 +224,83 @@ function apply(ctx) {
|
|
|
162
224
|
}
|
|
163
225
|
const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
|
|
164
226
|
const method = pathname.startsWith(prefix) ? pathname.slice(prefix.length) : void 0;
|
|
227
|
+
if (method === "config.get") {
|
|
228
|
+
writeJson(res, 200, {
|
|
229
|
+
ok: true,
|
|
230
|
+
value: liveConfig
|
|
231
|
+
});
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (method === "config.set") {
|
|
235
|
+
try {
|
|
236
|
+
const body = await readJsonBody(req);
|
|
237
|
+
if (typeof body.maxDepth === "number" && body.maxDepth > 0) liveConfig.maxDepth = body.maxDepth;
|
|
238
|
+
if (typeof body.showHidden === "boolean") liveConfig.showHidden = body.showHidden;
|
|
239
|
+
writeJson(res, 200, {
|
|
240
|
+
ok: true,
|
|
241
|
+
value: liveConfig
|
|
242
|
+
});
|
|
243
|
+
} catch (err) {
|
|
244
|
+
writeJson(res, 500, {
|
|
245
|
+
ok: false,
|
|
246
|
+
error: { message: err?.message || String(err) }
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (method === "files.open") {
|
|
252
|
+
try {
|
|
253
|
+
const body = await readJsonBody(req);
|
|
254
|
+
const cwd = resolveCwd(ctx, body);
|
|
255
|
+
const rawPath = String(body.path || "");
|
|
256
|
+
if (!rawPath) {
|
|
257
|
+
writeJson(res, 400, {
|
|
258
|
+
ok: false,
|
|
259
|
+
error: { message: "Missing path parameter" }
|
|
260
|
+
});
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const fullPath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
|
|
264
|
+
const action = body.action || "reveal";
|
|
265
|
+
const isDirectory = Boolean(body.isDirectory);
|
|
266
|
+
if (process.platform === "win32") {
|
|
267
|
+
const winPath = fullPath.replace(/\//g, "\\");
|
|
268
|
+
if (action === "reveal") {
|
|
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 });
|
|
278
|
+
} else if (process.platform === "darwin") {
|
|
279
|
+
if (action === "reveal") spawn("open", [isDirectory ? fullPath : "-R", fullPath], {
|
|
280
|
+
detached: true,
|
|
281
|
+
stdio: "ignore"
|
|
282
|
+
}).unref();
|
|
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
|
+
writeJson(res, 200, { ok: true });
|
|
296
|
+
} catch (err) {
|
|
297
|
+
writeJson(res, 500, {
|
|
298
|
+
ok: false,
|
|
299
|
+
error: { message: err?.message || String(err) }
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
165
304
|
if (method !== "files.tree") {
|
|
166
305
|
writeJson(res, 404, {
|
|
167
306
|
ok: false,
|
|
@@ -171,13 +310,36 @@ function apply(ctx) {
|
|
|
171
310
|
}
|
|
172
311
|
try {
|
|
173
312
|
const body = await readJsonBody(req);
|
|
313
|
+
const cwd = resolveCwd(ctx, body);
|
|
314
|
+
const showHidden = typeof body.showHidden === "boolean" ? body.showHidden : liveConfig.showHidden;
|
|
315
|
+
const showIgnored = Boolean(body.showIgnored);
|
|
316
|
+
const maxDepth = typeof body.maxDepth === "number" && body.maxDepth > 0 ? body.maxDepth : liveConfig.maxDepth;
|
|
317
|
+
const forceRefresh = Boolean(body.forceRefresh);
|
|
318
|
+
const cacheKey = `${cwd}::${showHidden}::${showIgnored}::${maxDepth}`;
|
|
319
|
+
const now = Date.now();
|
|
320
|
+
if (!forceRefresh) {
|
|
321
|
+
const cached = scanCache.get(cacheKey);
|
|
322
|
+
if (cached && now - cached.timestamp < CACHE_TTL_MS) {
|
|
323
|
+
writeJson(res, 200, {
|
|
324
|
+
ok: true,
|
|
325
|
+
value: cached.result,
|
|
326
|
+
cached: true
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const result = await scanWorkspaceTree(cwd, {
|
|
332
|
+
showHidden,
|
|
333
|
+
showIgnored,
|
|
334
|
+
maxDepth
|
|
335
|
+
});
|
|
336
|
+
scanCache.set(cacheKey, {
|
|
337
|
+
timestamp: now,
|
|
338
|
+
result
|
|
339
|
+
});
|
|
174
340
|
writeJson(res, 200, {
|
|
175
341
|
ok: true,
|
|
176
|
-
value:
|
|
177
|
-
showHidden: Boolean(body.showHidden),
|
|
178
|
-
showIgnored: Boolean(body.showIgnored),
|
|
179
|
-
maxDepth: typeof body.maxDepth === "number" ? body.maxDepth : 8
|
|
180
|
-
})
|
|
342
|
+
value: result
|
|
181
343
|
});
|
|
182
344
|
} catch (err) {
|
|
183
345
|
writeJson(res, 500, {
|
|
@@ -193,4 +355,4 @@ function apply(ctx) {
|
|
|
193
355
|
}), "dsh-workspace-files: /dsh-workspace-files/api routes");
|
|
194
356
|
}
|
|
195
357
|
//#endregion
|
|
196
|
-
export { apply, inject, name };
|
|
358
|
+
export { Config, SETTINGS_NS, apply, inject, name };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@civilization/dsh-workspace-files",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Enhanced workspace file tree explorer with fuzzy search and @-mention for DeepSeek Harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
},
|
|
26
26
|
"client": {
|
|
27
27
|
"platform": "web",
|
|
28
|
-
"inject": [
|
|
28
|
+
"inject": [
|
|
29
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
30
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins"
|
|
31
|
+
]
|
|
29
32
|
}
|
|
30
33
|
},
|
|
31
34
|
"scripts": {
|
|
@@ -33,6 +36,9 @@
|
|
|
33
36
|
"dev": "tsdown --watch",
|
|
34
37
|
"typecheck": "tsc --noEmit"
|
|
35
38
|
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@deepseek-ai/schemastery": "3.18.2"
|
|
41
|
+
},
|
|
36
42
|
"peerDependencies": {
|
|
37
43
|
"@deepseek-ai/cordis": ">=4.0.0",
|
|
38
44
|
"react": ">=18.0.0"
|
package/src/client/icons.tsx
CHANGED
|
@@ -159,3 +159,30 @@ export function IconTree({ size = 13 }: { size?: number }): JSX.Element {
|
|
|
159
159
|
)
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
export function IconChevronDown({ size = 14 }: { size?: number }): JSX.Element {
|
|
163
|
+
return (
|
|
164
|
+
<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 }}>
|
|
165
|
+
<polyline points="4 6 8 10 12 6" />
|
|
166
|
+
</svg>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function IconExplorer({ size = 12 }: { size?: number }): JSX.Element {
|
|
171
|
+
return (
|
|
172
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="currentColor" style={{ display: 'inline-block', verticalAlign: 'middle', flexShrink: 0 }}>
|
|
173
|
+
<path d="M1.75 2A1.75 1.75 0 000 3.75v8.5C0 13.216.784 14 1.75 14h12.5A1.75 1.75 0 0016 12.25v-6.5A1.75 1.75 0 0014.25 4H7.5a.25.25 0 01-.2-.1l-.9-1.2C6.07 2.26 5.55 2 5 2H1.75zM1.5 5.5h13v6.75a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25V5.5z" />
|
|
174
|
+
</svg>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function IconExternalApp({ size = 12 }: { size?: number }): JSX.Element {
|
|
179
|
+
return (
|
|
180
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.35" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'inline-block', verticalAlign: 'middle', flexShrink: 0 }}>
|
|
181
|
+
<path d="M12.5 9.5v3.25a1.25 1.25 0 01-1.25 1.25H3.25A1.25 1.25 0 012 12.75V4.75A1.25 1.25 0 013.25 3.5H6.5" />
|
|
182
|
+
<polyline points="10 2 14 2 14 6" />
|
|
183
|
+
<line x1="6.5" y1="9.5" x2="13.5" y2="2.5" />
|
|
184
|
+
</svg>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|