dsh-code-ui 1.6.0 → 1.6.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.
Files changed (2) hide show
  1. package/lib/index.js +54 -10
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -8,7 +8,7 @@ import { Remote, bindTypertRemote } from "@deepseek-ai/dsh-typert-protocol";
8
8
  import { defineTool } from "@deepseek-ai/dsh-tools";
9
9
  import z from "@deepseek-ai/schemastery";
10
10
  import { spawn } from "node:child_process";
11
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
11
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
12
12
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
14
 
@@ -696,7 +696,48 @@ function unwatchAllFiles() {
696
696
  }
697
697
 
698
698
  function rpcHandler(ctx) {
699
- const fs = ctx.get("fs");
699
+ // fs 服务延迟获取:插件 apply 可能早于 base bundle 的 fs-sandbox 行挂载,
700
+ // apply 时一次性 ctx.get("fs") 会永久取到 undefined(0.2.0-rc.2 实测:
701
+ // list-dir/stat-path/read-file 全部抛 "Cannot read properties of
702
+ // undefined (reading 'resolve')")。改为 inject 等待 + 调用时解析。
703
+ let fsSvc = null;
704
+ try { fsSvc = ctx.get("fs") || null; } catch (_) {}
705
+ try {
706
+ ctx.inject(["fs"], (c) => { fsSvc = c.fs || fsSvc; });
707
+ } catch (_) {}
708
+ // 宿主组合始终无 fs 服务时的原生兜底:与 fs-mkdir/write-file 等磁盘端点
709
+ // 同一权限模型(编辑器是用户显式操作面,本就读写真实磁盘)。
710
+ const nativeFsShim = {
711
+ resolve: async (p) => p,
712
+ stat: async (p) => {
713
+ try {
714
+ const s = statSync(p);
715
+ return { type: s.isDirectory() ? "directory" : s.isFile() ? "file" : "other" };
716
+ } catch (_) { return null; }
717
+ },
718
+ listDir: async (p) => {
719
+ const sep = String(p).includes("\\") ? "\\" : "/";
720
+ return readdirSync(p, { withFileTypes: true }).map((d) => ({
721
+ name: d.name,
722
+ path: String(p).replace(/[\\/]+$/, "") + sep + d.name,
723
+ isDir: d.isDirectory(),
724
+ }));
725
+ },
726
+ readBytes: async (p, _signal, maxBytes) => {
727
+ const b = readFileSync(p);
728
+ if (b.length > maxBytes) {
729
+ const e = new Error("file too large");
730
+ e.code = "FS_TOO_LARGE";
731
+ throw e;
732
+ }
733
+ return new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
734
+ },
735
+ readByteRange: async (p, range) => {
736
+ const b = readFileSync(p);
737
+ return new Uint8Array(b.buffer, b.byteOffset, b.byteLength).slice(range.offset, range.offset + range.length);
738
+ },
739
+ };
740
+ const getFs = () => fsSvc || nativeFsShim;
700
741
  let currentRootRef = detectWorkspaceRoot(ctx); // 最近一次解析的根:list-dir 的兜底
701
742
  // 每会话根目录记忆:sessionId -> 手动设置的根(「以此文件夹为根」)。
702
743
  // 按会话隔离——手动根只影响设置它的那个会话,不再全局污染其它会话的自动解析;
@@ -792,8 +833,9 @@ function rpcHandler(ctx) {
792
833
  if (endpoint === "list-dir") {
793
834
  const path = args.path || currentRootRef;
794
835
  if (!path) return ok({ error: "no-root" });
795
- const target = await fs.resolve(path);
796
- const entries = await fs.listDir(target);
836
+ const fsvc = getFs();
837
+ const target = await fsvc.resolve(path);
838
+ const entries = await fsvc.listDir(target);
797
839
  const sep = String(path).includes("\\") ? "\\" : "/";
798
840
  const list = entries.map((e) => {
799
841
  let name = null;
@@ -820,8 +862,9 @@ function rpcHandler(ctx) {
820
862
 
821
863
  if (endpoint === "stat-path") {
822
864
  if (!args.path) return ok({ error: "no-path" });
823
- const target = await fs.resolve(args.path);
824
- const info = await fs.stat(target);
865
+ const fsvc = getFs();
866
+ const target = await fsvc.resolve(args.path);
867
+ const info = await fsvc.stat(target);
825
868
  // DSH fs.stat 返回 FsInfo { type: 'file' | 'directory' | 'other' },
826
869
  // 没有 isDirectory/isFile 布尔字段;必须按 type 判定(原实现恒为 false,
827
870
  // 导致目录路径被误报为「不存在」)。保留遗留字段兼容。
@@ -833,19 +876,20 @@ function rpcHandler(ctx) {
833
876
 
834
877
  if (endpoint === "read-file") {
835
878
  if (!args.path) return ok({ error: "no-path" });
836
- const target = await fs.resolve(args.path);
879
+ const fsvc = getFs();
880
+ const target = await fsvc.resolve(args.path);
837
881
  // 对齐 dsh ≥ 0.1.7-rc.1 的二进制 Remote 结果契约:字节经 Uint8Array 字段返回,
838
882
  // gateway 自动投影为 multipart 附件下发;0.1.5/0.1.6 老宿主无投影则走 JSON
839
883
  // (Uint8Array 落成下标对象,客户端负责重建),同一份代码两端兼容。
840
884
  // 取代旧的 60 万字符文本截断——截断内容若被 Ctrl+S 原样写回会写坏大文件。
841
885
  const MAX = 4 * 1024 * 1024; // 单次读取上限 4MB
842
886
  try {
843
- const bytes = await fs.readBytes(target, undefined, MAX);
887
+ const bytes = await fsvc.readBytes(target, undefined, MAX);
844
888
  return ok({ path: args.path, bytes, truncated: false, size: bytes.length });
845
889
  } catch (err) {
846
- if (!err || err.code !== "FS_TOO_LARGE" || typeof fs.readByteRange !== "function") throw err;
890
+ if (!err || err.code !== "FS_TOO_LARGE" || typeof fsvc.readByteRange !== "function") throw err;
847
891
  // 超限文件读前 4MB,标记 truncated;客户端据此禁写并提示
848
- const bytes = await fs.readByteRange(target, { offset: 0, length: MAX });
892
+ const bytes = await fsvc.readByteRange(target, { offset: 0, length: MAX });
849
893
  return ok({ path: args.path, bytes, truncated: true });
850
894
  }
851
895
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-code-ui",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "Cursor 风格的 AI 代码编辑器工作台插件(DSH):文件树、多标签页编辑、语法高亮、引用/翻译行内标签给 AI、右侧停靠/悬浮窗口双布局",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",