dsh-explorer 0.1.0 → 0.1.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/lib/client.js +817 -2772
- package/lib/index.js +124 -17
- package/package.json +1 -2
- package/src/client/ExplorerView.tsx +494 -652
- package/src/client/{ExplorerContextMenu.tsx → components/ExplorerContextMenu.tsx} +2 -4
- package/src/client/components/ExplorerDeleteModal.tsx +124 -0
- package/src/client/components/ExplorerEmptyContextMenu.tsx +125 -0
- package/src/client/{ExplorerHeader.tsx → components/ExplorerHeader.tsx} +18 -12
- package/src/client/{ExplorerOptionsModal.tsx → components/ExplorerOptionsModal.tsx} +2 -15
- package/src/client/{ExplorerRow.tsx → components/ExplorerRow.tsx} +45 -27
- package/src/client/{ExplorerSearch.tsx → components/ExplorerSearch.tsx} +18 -5
- package/src/client/components/TreeNode.tsx +384 -0
- package/src/client/explorer.module.css +215 -10
- package/src/client/hooks/useKeyboardNav.ts +89 -0
- package/src/client/hooks/useLiveSync.ts +81 -0
- package/src/client/index.tsx +82 -4
- package/src/client/{api.ts → utils/api.ts} +133 -3
- package/src/client/{conversation-draft.ts → utils/conversation-draft.ts} +38 -29
- package/src/client/utils/open-tab.ts +69 -0
- package/src/fs-search.ts +116 -0
- package/src/git-status.ts +19 -13
- package/src/index.ts +12 -0
- /package/src/client/{drag-drop.ts → utils/drag-drop.ts} +0 -0
- /package/src/client/{options.ts → utils/options.ts} +0 -0
- /package/src/client/{vscode-theme.tsx → utils/vscode-theme.tsx} +0 -0
package/lib/index.js
CHANGED
|
@@ -339,12 +339,97 @@ function createFsWatcherServer() {
|
|
|
339
339
|
};
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
// src/fs-search.ts
|
|
343
|
+
import { readdir as readdir2 } from "node:fs/promises";
|
|
344
|
+
import { join as join2, relative } from "node:path";
|
|
345
|
+
var IGNORED_SEARCH_DIRS = /* @__PURE__ */ new Set([
|
|
346
|
+
"node_modules",
|
|
347
|
+
".agent",
|
|
348
|
+
".git",
|
|
349
|
+
"dist",
|
|
350
|
+
"target",
|
|
351
|
+
"build",
|
|
352
|
+
".system_generated",
|
|
353
|
+
".cache",
|
|
354
|
+
".dsh",
|
|
355
|
+
".gemini",
|
|
356
|
+
".vscode",
|
|
357
|
+
".idea"
|
|
358
|
+
]);
|
|
359
|
+
async function searchDirectory(rootDir, query, maxResults = 250, includeHidden = false) {
|
|
360
|
+
const cleanQuery = query.trim().toLowerCase();
|
|
361
|
+
if (!cleanQuery) {
|
|
362
|
+
return { query, matches: [], totalMatches: 0, truncated: false };
|
|
363
|
+
}
|
|
364
|
+
const isExtensionFilter = cleanQuery.startsWith("*.") && cleanQuery.length > 2;
|
|
365
|
+
const extensionTarget = isExtensionFilter ? cleanQuery.slice(1) : "";
|
|
366
|
+
const matches = [];
|
|
367
|
+
let truncated = false;
|
|
368
|
+
let scannedCount = 0;
|
|
369
|
+
const maxScanned = 3e4;
|
|
370
|
+
const queue = [rootDir];
|
|
371
|
+
while (queue.length > 0 && matches.length < maxResults && scannedCount < maxScanned) {
|
|
372
|
+
const currentDir = queue.shift();
|
|
373
|
+
let entries;
|
|
374
|
+
try {
|
|
375
|
+
entries = await readdir2(currentDir, { withFileTypes: true });
|
|
376
|
+
} catch {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
for (const ent of entries) {
|
|
380
|
+
scannedCount++;
|
|
381
|
+
if (scannedCount >= maxScanned) {
|
|
382
|
+
truncated = true;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
if (!includeHidden && ent.name.startsWith(".")) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const isDirectory = ent.isDirectory();
|
|
389
|
+
if (isDirectory && IGNORED_SEARCH_DIRS.has(ent.name)) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const fullPath = join2(currentDir, ent.name);
|
|
393
|
+
const relPath = relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
394
|
+
const nameLower = ent.name.toLowerCase();
|
|
395
|
+
const relLower = relPath.toLowerCase();
|
|
396
|
+
let isMatch = false;
|
|
397
|
+
if (isExtensionFilter) {
|
|
398
|
+
isMatch = !isDirectory && nameLower.endsWith(extensionTarget);
|
|
399
|
+
} else {
|
|
400
|
+
isMatch = nameLower.includes(cleanQuery) || relLower.includes(cleanQuery);
|
|
401
|
+
}
|
|
402
|
+
if (isMatch) {
|
|
403
|
+
matches.push({
|
|
404
|
+
name: ent.name,
|
|
405
|
+
path: fullPath,
|
|
406
|
+
relativePath: relPath,
|
|
407
|
+
isDir: isDirectory
|
|
408
|
+
});
|
|
409
|
+
if (matches.length >= maxResults) {
|
|
410
|
+
truncated = true;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (isDirectory) {
|
|
415
|
+
queue.push(fullPath);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
query,
|
|
421
|
+
matches,
|
|
422
|
+
totalMatches: matches.length,
|
|
423
|
+
truncated
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
342
427
|
// src/fs-operations.ts
|
|
343
428
|
import { randomUUID } from "node:crypto";
|
|
344
429
|
import { once } from "node:events";
|
|
345
430
|
import { createWriteStream } from "node:fs";
|
|
346
431
|
import { access, lstat, mkdir, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
347
|
-
import { basename, dirname, join as
|
|
432
|
+
import { basename, dirname, join as join3 } from "node:path";
|
|
348
433
|
|
|
349
434
|
// src/path-security.ts
|
|
350
435
|
import { homedir } from "node:os";
|
|
@@ -369,7 +454,7 @@ function assertSingleSegmentName(name2) {
|
|
|
369
454
|
// src/fs-operations.ts
|
|
370
455
|
async function createNewFile(dirPath, name2) {
|
|
371
456
|
assertSingleSegmentName(name2);
|
|
372
|
-
const fullPath =
|
|
457
|
+
const fullPath = join3(dirPath, name2);
|
|
373
458
|
try {
|
|
374
459
|
await access(fullPath);
|
|
375
460
|
throw new ExplorerError("already-exists", `file "${name2}" already exists`, 409);
|
|
@@ -382,7 +467,7 @@ async function createNewFile(dirPath, name2) {
|
|
|
382
467
|
}
|
|
383
468
|
async function createDirectory(dirPath, name2) {
|
|
384
469
|
assertSingleSegmentName(name2);
|
|
385
|
-
const fullPath =
|
|
470
|
+
const fullPath = join3(dirPath, name2);
|
|
386
471
|
try {
|
|
387
472
|
await access(fullPath);
|
|
388
473
|
throw new ExplorerError("already-exists", `directory "${name2}" already exists`, 409);
|
|
@@ -396,7 +481,7 @@ async function createDirectory(dirPath, name2) {
|
|
|
396
481
|
async function renameEntry(targetPath, newName) {
|
|
397
482
|
assertSingleSegmentName(newName);
|
|
398
483
|
const parent = dirname(targetPath);
|
|
399
|
-
const newPath =
|
|
484
|
+
const newPath = join3(parent, newName);
|
|
400
485
|
if (targetPath === newPath) return { oldPath: targetPath, newPath };
|
|
401
486
|
try {
|
|
402
487
|
await access(newPath);
|
|
@@ -436,8 +521,8 @@ async function writeUpload(baseDir, relativePath, streamChunks) {
|
|
|
436
521
|
if (segments.some((p) => !p || p === "." || p === "..")) {
|
|
437
522
|
throw new ExplorerError("bad-request", "relativePath contains invalid path traversal segments", 400);
|
|
438
523
|
}
|
|
439
|
-
const target =
|
|
440
|
-
const tmp =
|
|
524
|
+
const target = join3(baseDir, ...segments);
|
|
525
|
+
const tmp = join3(dirname(target), `.${basename(target)}.dsh-upload-${randomUUID()}.tmp`);
|
|
441
526
|
await mkdir(dirname(target), { recursive: true });
|
|
442
527
|
const writeStream = createWriteStream(tmp, { flags: "wx" });
|
|
443
528
|
let bytes = 0;
|
|
@@ -467,7 +552,7 @@ async function writeUpload(baseDir, relativePath, streamChunks) {
|
|
|
467
552
|
// src/git-status.ts
|
|
468
553
|
import { execFile } from "node:child_process";
|
|
469
554
|
import { existsSync, readdirSync } from "node:fs";
|
|
470
|
-
import { join as
|
|
555
|
+
import { join as join4, relative as relative2 } from "node:path";
|
|
471
556
|
import { promisify } from "node:util";
|
|
472
557
|
var execFileAsync = promisify(execFile);
|
|
473
558
|
var gitCache = /* @__PURE__ */ new Map();
|
|
@@ -512,8 +597,9 @@ function parsePorcelainLines(stdout, prefix = "") {
|
|
|
512
597
|
}
|
|
513
598
|
}
|
|
514
599
|
}
|
|
515
|
-
const cleanPath = p.replace(/\\/g, "/");
|
|
516
|
-
const
|
|
600
|
+
const cleanPath = p.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
601
|
+
const cleanPrefix = prefix ? prefix.replace(/\\/g, "/").replace(/\/+$/, "").replace(/^\.\//, "") : "";
|
|
602
|
+
const finalPath = cleanPrefix ? `${cleanPrefix}/${cleanPath}` : cleanPath;
|
|
517
603
|
entries.push({ path: finalPath, xy });
|
|
518
604
|
}
|
|
519
605
|
return { branch, entries };
|
|
@@ -527,8 +613,8 @@ function findSubRepos(root, depth = 1, maxDepth = 3) {
|
|
|
527
613
|
if (!ent.isDirectory() || ent.name.startsWith(".") || IGNORED_SCAN_DIRS.has(ent.name)) {
|
|
528
614
|
continue;
|
|
529
615
|
}
|
|
530
|
-
const full =
|
|
531
|
-
if (existsSync(
|
|
616
|
+
const full = join4(root, ent.name);
|
|
617
|
+
if (existsSync(join4(full, ".git"))) {
|
|
532
618
|
repos.push(full);
|
|
533
619
|
} else {
|
|
534
620
|
repos.push(...findSubRepos(full, depth + 1, maxDepth));
|
|
@@ -545,11 +631,22 @@ async function getGitStatus(cwd) {
|
|
|
545
631
|
return cached.data;
|
|
546
632
|
}
|
|
547
633
|
try {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
634
|
+
let stdout = "";
|
|
635
|
+
try {
|
|
636
|
+
const res = await execFileAsync(
|
|
637
|
+
"git",
|
|
638
|
+
["status", "--porcelain", "-b", "-unormal", "--ignored=matching"],
|
|
639
|
+
{ cwd, maxBuffer: 10 * 1024 * 1024, timeout: 3500 }
|
|
640
|
+
);
|
|
641
|
+
stdout = res.stdout;
|
|
642
|
+
} catch {
|
|
643
|
+
const res = await execFileAsync(
|
|
644
|
+
"git",
|
|
645
|
+
["status", "--porcelain", "-b", "-unormal"],
|
|
646
|
+
{ cwd, maxBuffer: 10 * 1024 * 1024, timeout: 3500 }
|
|
647
|
+
);
|
|
648
|
+
stdout = res.stdout;
|
|
649
|
+
}
|
|
553
650
|
const parsed = parsePorcelainLines(stdout);
|
|
554
651
|
const data = {
|
|
555
652
|
isRepo: true,
|
|
@@ -566,7 +663,7 @@ async function getGitStatus(cwd) {
|
|
|
566
663
|
const allEntries = [];
|
|
567
664
|
const subResults = await Promise.all(
|
|
568
665
|
subRepos.map(async (repoDir) => {
|
|
569
|
-
const rel =
|
|
666
|
+
const rel = relative2(cwd, repoDir).replace(/\\/g, "/");
|
|
570
667
|
try {
|
|
571
668
|
const { stdout } = await execFileAsync(
|
|
572
669
|
"git",
|
|
@@ -686,6 +783,16 @@ function apply(ctx, config) {
|
|
|
686
783
|
writeOk(res, { git });
|
|
687
784
|
return true;
|
|
688
785
|
}
|
|
786
|
+
if (pathname === "/dsh-explorer/api/search") {
|
|
787
|
+
const sessionId = url.searchParams.get("sessionId") ?? void 0;
|
|
788
|
+
const cwd = resolveSessionCwd(ctx, sessionId);
|
|
789
|
+
const dirParam = url.searchParams.get("dir");
|
|
790
|
+
const q = url.searchParams.get("q") ?? "";
|
|
791
|
+
const targetDir = resolveTarget(cwd, dirParam ?? cwd);
|
|
792
|
+
const result = await searchDirectory(targetDir, q);
|
|
793
|
+
writeOk(res, { result });
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
689
796
|
if (pathname === "/dsh-explorer/api/read-file") {
|
|
690
797
|
const pathParam = url.searchParams.get("path");
|
|
691
798
|
if (!pathParam) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-explorer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Native workspace file explorer for DeepSeek Harness with precision drag-and-drop chat insertion, motion collapse, git status badges, compact folders, and tab integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -91,4 +91,3 @@
|
|
|
91
91
|
"typecheck": "tsc --noEmit"
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
-
|