dsh-vscode-mode 0.5.1 → 0.5.2

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 CHANGED
@@ -347,13 +347,13 @@ TortoiseSVN(`TortoiseProc.exe`)仅作 Windows 过渡增强:自研能力覆
347
347
 
348
348
  ```bash
349
349
  # ① Git 安装(clone + prepare 构建;推荐打固定 tag)
350
- dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.5.1
350
+ dsh plugin --profile web add github:Lenonss/DSH_VsCodeMode#v0.5.2
351
351
 
352
352
  # ② npm 注册表(发布到 npm 后)
353
353
  dsh plugin --profile web add dsh-vscode-mode
354
354
 
355
355
  # ③ GitHub Release tgz 直装
356
- dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.5.1/dsh-vscode-mode-0.5.1.tgz
356
+ dsh plugin --profile web add https://github.com/Lenonss/DSH_VsCodeMode/releases/download/v0.5.2/dsh-vscode-mode-0.5.2.tgz
357
357
  ```
358
358
 
359
359
  > `dsh plugin ...` 是 pnpm 转发器:git 安装会克隆仓库、执行该包 `prepare` 脚本
@@ -640,6 +640,13 @@ Keep All / Undo All 却是亮的。修复:单文件 Keep / Undo 覆盖冲突
640
640
  完整变更见 [GitHub Releases](https://github.com/Lenonss/DSH_VsCodeMode/releases)。
641
641
  近期关键版本:
642
642
 
643
+ - **v0.5.2**:**修复 macOS / npm 安装形态的兼容问题**——① schema 库按候选链解析
644
+ (官方已把 vendored schemastery 改名为 `@deepseek-ai/schemastery`,安装树里没有裸
645
+ `schemastery`),且 `@deepseek-ai/dsh-settings` 缺失不再拖垮整体 → 修复从 npm 安装时
646
+ 「设置 section 尚未装配 / 设置持久化未安装」(开发形态因有 devDependency 副本而不复现);
647
+ ② VSIX 解包保留归档自带 Unix 执行位并按入口名兜底补 0o755 → 修复 macOS/Linux 上语言
648
+ 服务器 spawn `EACCES` 起不来;③ 删除新建文件按平台分派删除命令(macOS/Linux 不再先试
649
+ PowerShell 再回落)。
643
650
  - **v0.5.1**:**适配 DSH 0.1.6-alpha.2**——会话作用域三级取值链(`uiSession.current`
644
651
  优先,兼容旧版 `sessions.list.current`;修复官方移除 `current` 后 Monaco 预热 / LSP
645
652
  同步 / 编辑 Tab 恢复 / 文件链接静默失效);Office 与官方「不可预览」后缀让位官方
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { chmod, copyFile, cp, mkdir, readFile, readdir, rename, rm, stat, truncate, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, extname, join, normalize, resolve, sep } from "node:path";
4
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, watch, writeFileSync } from "node:fs";
4
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, watch, writeFileSync } from "node:fs";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { homedir, tmpdir } from "node:os";
7
7
  import { createHash, randomUUID } from "node:crypto";
@@ -330,17 +330,83 @@ async function hostImport(specifier) {
330
330
  return import(specifier);
331
331
  }
332
332
  /**
333
- * 动态加载设置依赖(模块级缓存;任一缺失/失败返回 null 而非抛错)。
334
- * installSettingsSection 仅在 rc 线 dsh-settings 中存在时提供(alpha 起移除,
335
- * 属性探测得到 undefined,不抛错)。
336
- * @author ddj 2026年08月24号 / 2026年09月15号
333
+ * schema 库候选名(新名在前)。
334
+ * DSH 官方自 0.1.5 起把 vendored 包改名为 @deepseek-ai/schemastery 并全树改用新名,
335
+ * 安装树里**没有**裸 schemastery;裸名保留给 rc 线(其 dsh-settings 仍 peers 旧名),
336
+ * 也让开发形态(插件 node_modules devDependency 副本)继续可用。
337
+ */
338
+ const SCHEMA_SPECIFIERS = ["@deepseek-ai/schemastery", "schemastery"];
339
+ /** 设置持久化包名(installSettingsSection 仅 rc 线提供,alpha 线缺失属正常)。 */
340
+ const SETTINGS_SPECIFIER = "@deepseek-ai/dsh-settings";
341
+ /**
342
+ * 逐个尝试候选名,返回首个加载成功的模块及其包名(全失败返回 null,不抛错)。
343
+ * @author ddj 2026年09月18号
344
+ * @param specifiers 候选包名(按优先级)
345
+ * @param importFn 加载函数(测试注入)
346
+ * @returns 命中的模块与包名;全失败返回 null
347
+ */
348
+ async function firstImport(specifiers, importFn = hostImport) {
349
+ for (const specifier of specifiers) try {
350
+ return {
351
+ specifier,
352
+ module: await importFn(specifier)
353
+ };
354
+ } catch {}
355
+ return null;
356
+ }
357
+ /**
358
+ * 抹平 schema 库的 ESM/CJS 互操作形态取默认导出。
359
+ * 三种实测形态:真 ESM(`default` 即 z)、CJS 经 import()(`default` 与
360
+ * `module.exports` 同为 z)、双层包装(`default.default` 才是 z)。
361
+ * @author ddj 2026年09月18号
362
+ * @param module 加载到的模块命名空间(可空)
363
+ * @returns 具备 object/string 等构造器的 z;取不到返回 null
364
+ */
365
+ function pickSchema(module) {
366
+ const layers = [
367
+ module,
368
+ module?.default,
369
+ module?.["module.exports"]
370
+ ];
371
+ for (const layer of layers) {
372
+ const z = layer?.default ?? layer;
373
+ if (z && typeof z.object === "function" && typeof z.string === "function") return z;
374
+ }
375
+ return null;
376
+ }
377
+ /** 实际命中的 schema 库名(供兼容性报告展示;未命中为空串)。 */
378
+ let schemaLib = "";
379
+ /**
380
+ * 读取实际命中的 schema 库名(报告文案用)。
381
+ * @author ddj 2026年09月18号
382
+ * @returns 包名;未解析到为空串
383
+ */
384
+ function schemaLibName() {
385
+ return schemaLib;
386
+ }
387
+ /**
388
+ * 动态加载设置依赖(模块级缓存;schema 库缺失返回 null 而非抛错)。
389
+ * ⚠️ 两个依赖**独立解析**:@deepseek-ai/dsh-settings 仅 rc 线跑 legacy 策略时需要,
390
+ * alpha 线走 settings 服务 installSection 用不到它;原先 Promise.all 让该包缺失
391
+ * 拖垮整体 → 用户端(npm 安装)settings section 永不装配。
392
+ * 同理,schema 库按候选链解析(安装树只有新名 @deepseek-ai/schemastery)。
393
+ * @author ddj 2026年08月24号 / 2026年09月15号 / 2026年09月18号
394
+ * @param importFn 加载函数(测试注入;缺省宿主锚点动态导入)
337
395
  * @returns 设置依赖或 null
338
396
  */
339
- function loadSettingsDeps() {
340
- if (!depsPromise) depsPromise = Promise.all([hostImport("@deepseek-ai/dsh-settings"), hostImport("schemastery")]).then(([dshSettings, schemastery]) => ({
341
- installSettingsSection: dshSettings.installSettingsSection,
342
- z: schemastery.default
343
- })).catch(() => null);
397
+ function loadSettingsDeps(importFn = hostImport) {
398
+ if (!depsPromise) depsPromise = Promise.all([firstImport([SETTINGS_SPECIFIER], importFn), firstImport(SCHEMA_SPECIFIERS, importFn)]).then(([settingsHit, schemaHit]) => {
399
+ const z = pickSchema(schemaHit?.module);
400
+ if (!z) {
401
+ schemaLib = "";
402
+ return null;
403
+ }
404
+ schemaLib = schemaHit?.specifier ?? "";
405
+ return {
406
+ installSettingsSection: (settingsHit?.module)?.installSettingsSection,
407
+ z
408
+ };
409
+ }).catch(() => null);
344
410
  return depsPromise;
345
411
  }
346
412
  const INSTALL_UNMOUNTED = "设置 section 尚未装配";
@@ -1698,7 +1764,7 @@ function detectExternal(ctx, depsAvailable) {
1698
1764
  {
1699
1765
  name: "设置持久化(@deepseek-ai/dsh-settings)",
1700
1766
  active: depsAvailable,
1701
- note: depsAvailable ? "设置 section 已安装" : "未安装:fileOpenTool 持久化降级为配置值"
1767
+ note: depsAvailable ? "设置 section 已安装(schema: " + (schemaLibName() || "未知") + ")" : "未安装:fileOpenTool 持久化降级为配置值"
1702
1768
  },
1703
1769
  {
1704
1770
  name: "settings 服务",
@@ -4256,8 +4322,33 @@ function newContentSearcher(ctx) {
4256
4322
  //#endregion
4257
4323
  //#region src/revert.ts
4258
4324
  /**
4325
+ * 删除单文件的 argv(纯函数,platform 可注入便于单测)。
4326
+ * Windows 走 PowerShell Remove-Item;其余平台走 /bin/rm。
4327
+ * 原先无条件先发 powershell:macOS/Linux 上必然 spawn 失败后才回落,
4328
+ * 每删一个文件多一次无谓 spawn 与失败噪声。
4329
+ * @author ddj 2026年09月18号
4330
+ * @param absPath 绝对路径
4331
+ * @param platform 目标平台(缺省当前进程平台)
4332
+ * @returns 删除命令 argv
4333
+ */
4334
+ function removeFileArgv(absPath, platform = process.platform) {
4335
+ if (platform === "win32") return [
4336
+ "powershell",
4337
+ "-NoProfile",
4338
+ "-NonInteractive",
4339
+ "-Command",
4340
+ "Remove-Item -LiteralPath \"" + absPath + "\" -Force"
4341
+ ];
4342
+ return [
4343
+ "/bin/rm",
4344
+ "-f",
4345
+ "--",
4346
+ absPath
4347
+ ];
4348
+ }
4349
+ /**
4259
4350
  * 删除新建文件(拒绝创建时):subprocess 删除,路径先经 fs.contains 校验工作区边界。
4260
- * @author ddj 2026年08月20号
4351
+ * @author ddj 2026年08月20号 / 2026年09月18号
4261
4352
  * @returns 成功或失败原因
4262
4353
  */
4263
4354
  async function deleteCreated(ctx, session, record) {
@@ -4293,29 +4384,13 @@ async function deleteCreated(ctx, session, record) {
4293
4384
  }
4294
4385
  };
4295
4386
  try {
4296
- await attempt([
4297
- "powershell",
4298
- "-NoProfile",
4299
- "-NonInteractive",
4300
- "-Command",
4301
- "Remove-Item -LiteralPath \"" + p + "\" -Force"
4302
- ]);
4387
+ await attempt(removeFileArgv(p));
4303
4388
  return { ok: true };
4304
4389
  } catch (error) {
4305
- try {
4306
- await attempt([
4307
- "/bin/rm",
4308
- "-f",
4309
- "--",
4310
- p
4311
- ]);
4312
- return { ok: true };
4313
- } catch (error2) {
4314
- return {
4315
- ok: false,
4316
- error: "删除失败(文件仍存在):" + String(error) + " / " + String(error2)
4317
- };
4318
- }
4390
+ return {
4391
+ ok: false,
4392
+ error: "删除失败(文件仍存在):" + String(error)
4393
+ };
4319
4394
  }
4320
4395
  }
4321
4396
  /** 整调用回滚:write/编辑用 before 整文件恢复;新建文件转 deleteCreated。 */
@@ -9864,9 +9939,9 @@ const CDIR_SIG = 33639248;
9864
9939
  const LHDR_SIG = 67324752;
9865
9940
  /**
9866
9941
  * 解析 ZIP 缓冲区 → 条目列表(不解压内容,仅元数据)。
9867
- * @author ddj 2026年08月27号
9942
+ * @author ddj 2026年08月27号 / 2026年09月18号
9868
9943
  * @param buf zip 字节
9869
- * @returns 条目元数据
9944
+ * @returns 条目元数据(含 Unix mode,见 modeOf)
9870
9945
  */
9871
9946
  function zipEntries(buf) {
9872
9947
  const eocd = findEocd(buf);
@@ -9884,22 +9959,36 @@ function zipEntries(buf) {
9884
9959
  const commentLen = buf.readUInt16LE(offset + 32);
9885
9960
  const localOffset = buf.readUInt32LE(offset + 42);
9886
9961
  const name = buf.subarray(offset + 46, offset + 46 + nameLen).toString("utf8");
9962
+ const mode = modeOf(buf.readUInt32LE(offset + 38));
9887
9963
  out.push({
9888
9964
  name,
9889
9965
  method,
9890
9966
  compressed,
9891
9967
  uncompressed,
9892
- localOffset
9968
+ localOffset,
9969
+ ...mode === void 0 ? {} : { mode }
9893
9970
  });
9894
9971
  offset += 46 + nameLen + extraLen + commentLen;
9895
9972
  }
9896
9973
  return out;
9897
9974
  }
9898
9975
  /**
9976
+ * 中央目录 external attributes → Unix 权限位。
9977
+ * ZIP 把 Unix mode 放在 `st_mode << 16`(含 S_IFREG 等类型位),低 16 位仅 DOS 属性;
9978
+ * 故必须右移 16 再取低 9 位权限。非 Unix 归档(Windows 打包)该字段为 0 或纯 DOS 位。
9979
+ * @author ddj 2026年09月18号
9980
+ * @param attributes 中央目录 offset+38 的 4 字节
9981
+ * @returns 0o000~0o777 权限位;无有效 mode 返回 undefined
9982
+ */
9983
+ function modeOf(attributes) {
9984
+ const mode = attributes >>> 16 & 511;
9985
+ return mode === 0 ? void 0 : mode;
9986
+ }
9987
+ /**
9899
9988
  * 解压 ZIP → 条目列表(目录条目含尾部 /)。
9900
- * @author ddj 2026年08月27号
9989
+ * @author ddj 2026年08月27号 / 2026年09月18号
9901
9990
  * @param buf zip 字节
9902
- * @returns 条目(含内容)
9991
+ * @returns 条目(含内容与可选 mode)
9903
9992
  */
9904
9993
  function unzip(buf) {
9905
9994
  const metas = zipEntries(buf);
@@ -9919,7 +10008,8 @@ function unzip(buf) {
9919
10008
  out.push({
9920
10009
  path: meta.name,
9921
10010
  data,
9922
- isDirectory: isDir
10011
+ isDirectory: isDir,
10012
+ ...meta.mode === void 0 ? {} : { mode: meta.mode }
9923
10013
  });
9924
10014
  }
9925
10015
  return out;
@@ -9960,8 +10050,60 @@ function vsixManifest(vsix) {
9960
10050
  throw new Error("VSIX 缺少 extension/package.json");
9961
10051
  }
9962
10052
  /**
10053
+ * 判定解包路径是否为可执行入口(无 mode 时的启发式兜底)。
10054
+ * 只认确定语义:`bin/` 段下的文件、语言服务器/运行时常见可执行名、`.sh` 脚本。
10055
+ * @author ddj 2026年09月18号
10056
+ * @param rel 相对路径(/ 分隔)
10057
+ * @returns 是否应补执行位
10058
+ */
10059
+ function looksExec(rel) {
10060
+ const path = String(rel ?? "").replace(/\\/g, "/");
10061
+ if (!path) return false;
10062
+ if (path.split("/").includes("bin")) return true;
10063
+ if (/\.(sh|bash|zsh)$/i.test(path)) return true;
10064
+ const base = (path.split("/").pop() ?? "").replace(/\.exe$/i, "");
10065
+ return [
10066
+ "lua-language-server",
10067
+ "emmylua_ls",
10068
+ "OmniSharp",
10069
+ "dotnet",
10070
+ "node"
10071
+ ].includes(base);
10072
+ }
10073
+ /**
10074
+ * 计算解包后要施加的权限位(纯函数,可单测)。
10075
+ * 优先信任归档自带的 Unix mode 中的执行位(VS Code 自身解 VSIX 即此口径);
10076
+ * 归档无 mode(Windows 打包的 VSIX)时按路径启发式补 0o755;
10077
+ * 其余保持 undefined(交给 umask 默认,不越权改动)。
10078
+ * @author ddj 2026年09月18号
10079
+ * @param entry 解压条目
10080
+ * @returns 目标权限位;无需处理返回 undefined
10081
+ */
10082
+ function execModeOf(entry) {
10083
+ if (entry.isDirectory) return void 0;
10084
+ const mode = entry.mode;
10085
+ if (mode !== void 0 && (mode & 73) !== 0) return mode;
10086
+ return looksExec(entry.path) ? 493 : void 0;
10087
+ }
10088
+ /**
10089
+ * 落盘后补可执行位(POSIX 专属;Windows 无此概念,直接跳过)。
10090
+ * 必须 best-effort:只读文件系统/无权限时不能拖垮整个扩展安装。
10091
+ * @author ddj 2026年09月18号
10092
+ * @param target 落盘绝对路径
10093
+ * @param entry 解压条目
10094
+ * @param platform 目标平台(测试注入)
10095
+ */
10096
+ function applyExecBit(target, entry, platform = process.platform) {
10097
+ if (platform === "win32") return;
10098
+ const mode = execModeOf(entry);
10099
+ if (mode === void 0) return;
10100
+ try {
10101
+ chmodSync(target, mode);
10102
+ } catch (error) {}
10103
+ }
10104
+ /**
9963
10105
  * 解包 VSIX 到目录(仅取 extension/ 子树,剥前缀;返回清单)。
9964
- * @author ddj 2026年08月27号
10106
+ * @author ddj 2026年08月27号 / 2026年09月18号
9965
10107
  * @param vsix vsix 字节
9966
10108
  * @param dest 目标目录(已存在则先清空)
9967
10109
  * @returns 清单对象
@@ -9985,6 +10127,7 @@ function unpackVsix(vsix, dest) {
9985
10127
  }
9986
10128
  mkdirSync(dirname(target), { recursive: true });
9987
10129
  writeFileSync(target, entry.data);
10130
+ applyExecBit(target, entry);
9988
10131
  if (rel === "package.json") manifest = JSON.parse(entry.data.toString("utf8"));
9989
10132
  }
9990
10133
  if (!manifest) throw new Error("VSIX 解包后缺少 package.json");