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/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 join2 } from "node:path";
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 = join2(dirPath, name2);
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 = join2(dirPath, name2);
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 = join2(parent, newName);
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 = join2(baseDir, ...segments);
440
- const tmp = join2(dirname(target), `.${basename(target)}.dsh-upload-${randomUUID()}.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 join3, relative } from "node:path";
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 finalPath = prefix ? `${prefix.replace(/\\/g, "/").replace(/\/+$/, "")}/${cleanPath}` : cleanPath;
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 = join3(root, ent.name);
531
- if (existsSync(join3(full, ".git"))) {
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
- const { stdout } = await execFileAsync(
549
- "git",
550
- ["status", "--porcelain", "-b", "-unormal", "--ignored=matching"],
551
- { cwd, maxBuffer: 10 * 1024 * 1024, timeout: 3500 }
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 = relative(cwd, repoDir).replace(/\\/g, "/");
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.0",
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
-