dsh-vscode-mode 0.11.0 → 0.12.0

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
@@ -118,6 +118,8 @@ async function refreshMcp(ctx, id) {
118
118
  * `|` 连接多个候选(任一命中即触发),如 `Alt+ArrowLeft|Ctrl+Alt+-`。
119
119
  * 约束:本模块被 host 与 client 两半共用,**禁止 import client 侧模块**(React/浏览器 API)。
120
120
  * 与 client/ui/commandCatalog 的一致性由 tests/commands.test.ts 断言兜底。
121
+ * 官方机制(2026-10):SHORTCUT_PROFILES 描述各命令在官方 ctx.shortcuts 的 profile 默认键位
122
+ * (由 client 注册官方服务时使用);chordToBinding/bindingToChord 负责插件弦与官方绑定互转。
121
123
  * 作者 ddj 2026年08月26号 / 2026年09月10号
122
124
  */
123
125
  /** 命令 id → 默认键位(可为多候选)。命令目录以此为准,新增命令只需加一项。 */
@@ -900,6 +902,9 @@ async function updateSection(provider, ns, patch, expectedRevision) {
900
902
  * 构建设置 section schema(install 路径与插件 Config 声明共用,保证两代形状同源)。
901
903
  * 字段集 = fileOpenTool/keybindings/sidebarMinWidth/maxOpenEditors/integrationBaseUrl/
902
904
  * aiInline/aiProvider/aiModel/aiEffort/svnPath/tortoisePath/nativeOpenExts,全部带默认值
905
+ * (keybindings 自 2026-10 起废弃停写:键位持久化归官方 shortcuts 存储
906
+ * userData/keybindings.json / dsh.keybindings.v1;字段保留仅为旧档迁移读取与降级回滚,
907
+ * 见 client/shortcutsOfficial.ts 的 migrateLegacyKeybindings。)
903
908
  * (Config 启动校验在 undefined/空配置下自动填充,rc/alpha 两代 cordis 均通过)。
904
909
  * options.volatile:DSH 0.1.7 线的 Config 导出专用——SettingsForms.describe 只下发
905
910
  * 含 volatile 字段的 entry(volatileForm 门槛),不标记则 ns 永不出现在 describe、
@@ -5429,7 +5434,7 @@ async function restoreFile(ctx, session, rec) {
5429
5434
  //#region src/rules.ts
5430
5435
  /**
5431
5436
  * dsh-vscode-mode host — 规则管理(Codebuddy/Cursor 式 .mdc 规则)。
5432
- * - 存储:用户规则 ~/.dsh/rules/*.mdc;项目规则 <工作区>/.dsh/rules/*.mdc(随仓库共享)。
5437
+ * - 存储:用户规则 DSH_HOME/rules + ~/.dsh/rules(当前目录优先);项目规则 <工作区>/.dsh/rules。
5433
5438
  * - 格式:frontmatter(description / alwaysApply / globs / enabled)+ markdown 正文;
5434
5439
  * 类型映射 总是=always / 自动=auto(globs) / 手动=manual(仅索引);enabled 为本插件扩展字段,缺省 true。
5435
5440
  * - 生效:host 注册 systemPrompt.section(order 400),每次装配同步读取(mtime 缓存),
@@ -5452,7 +5457,7 @@ const SECTION_NAME = "dsh-vscode-mode:rules";
5452
5457
  /** section 顺序:persona(0) 与 PLAN_POLICY(500) 之间,规则先于计划策略被读到。 */
5453
5458
  const SECTION_ORDER = 400;
5454
5459
  /**
5455
- * 用户规则目录(~/.dsh/rules)。
5460
+ * 当前用户规则目录(DSH_HOME/rules)。
5456
5461
  * @author ddj 2026年09月03号
5457
5462
  * @param home DSH home(缺省自动解析)
5458
5463
  * @returns 绝对路径
@@ -5460,6 +5465,27 @@ const SECTION_ORDER = 400;
5460
5465
  function userRulesDir(home = dshHome()) {
5461
5466
  return join(home, "rules");
5462
5467
  }
5468
+ /** DSH_HOME 改变时仍读取系统用户目录的旧规则;相同路径不重复加载。 */
5469
+ function userRuleDirs(activeHome = dshHome(), systemHome = homedir()) {
5470
+ const current = resolve(userRulesDir(activeHome));
5471
+ const legacy = resolve(userRulesDir(join(systemHome, ".dsh")));
5472
+ const key = (path) => process.platform === "win32" ? path.toLowerCase() : path;
5473
+ return key(current) === key(legacy) ? { current } : {
5474
+ current,
5475
+ legacy
5476
+ };
5477
+ }
5478
+ function userDirOf(origin, dirs) {
5479
+ if (origin === void 0 || origin === "current") return dirs.current;
5480
+ if (origin === "legacy" && dirs.legacy) return dirs.legacy;
5481
+ throw new Error("无效的用户规则来源");
5482
+ }
5483
+ /** 同名时当前目录优先;禁用的当前规则也应遮蔽旧目录规则。 */
5484
+ function mergeUserRules(current, legacy, fileOf) {
5485
+ const key = (rule) => process.platform === "win32" ? fileOf(rule).toLowerCase() : fileOf(rule);
5486
+ const seen = new Set(current.map(key));
5487
+ return [...current, ...legacy.filter((rule) => !seen.has(key(rule)))].sort((a, b) => fileOf(a).localeCompare(fileOf(b)));
5488
+ }
5463
5489
  /**
5464
5490
  * 项目规则目录(<工作区>/.dsh/rules)。
5465
5491
  * @author ddj 2026年09月03号
@@ -5663,18 +5689,28 @@ function renderRulesSection(user, project, workspacePath) {
5663
5689
  return [renderScope("用户规则(用户配置,必须遵守)", user), workspacePath ? renderScope("项目规则(工作区 " + workspacePath + ",必须遵守)", project) : ""].filter(Boolean).join("\n");
5664
5690
  }
5665
5691
  /** 单目录规则元信息列表(仅 *.mdc,最多 RULE_DIR_CAP 个;目录缺失返回空)。 */
5666
- async function listRulesDir(dir, scope) {
5667
- const mdcs = (await readdir(dir).catch(() => [])).filter((name) => name.endsWith(".mdc")).sort().slice(0, RULE_DIR_CAP);
5692
+ async function listRulesDir(dir, scope, origin) {
5693
+ const mdcs = (await readdir(dir).catch((error) => {
5694
+ if (error.code === "ENOENT") return [];
5695
+ throw error;
5696
+ })).filter((name) => name.endsWith(".mdc")).sort().slice(0, RULE_DIR_CAP);
5668
5697
  const out = [];
5669
5698
  for (const file of mdcs) {
5670
5699
  const absPath = join(dir, file);
5671
- const info = await stat(absPath).catch(() => null);
5700
+ const info = await stat(absPath).catch((error) => {
5701
+ if (error.code === "ENOENT") return null;
5702
+ throw error;
5703
+ });
5672
5704
  if (!info || !info.isFile()) continue;
5673
- const parsed = parseRuleMdc(await readFile(absPath, "utf8").catch(() => ""));
5674
- out.push(toRuleInfo(scope, file, absPath, parsed, info.size, info.mtimeMs));
5705
+ const parsed = parseRuleMdc(await readFile(absPath, "utf8"));
5706
+ out.push(toRuleInfo(scope, file, absPath, parsed, info.size, info.mtimeMs, origin));
5675
5707
  }
5676
5708
  return out;
5677
5709
  }
5710
+ /** 列表与注入共用当前目录优先的合并约定。 */
5711
+ async function listUserRules(dirs = userRuleDirs()) {
5712
+ return mergeUserRules(await listRulesDir(dirs.current, "user", "current"), dirs.legacy ? await listRulesDir(dirs.legacy, "user", "legacy") : [], (rule) => rule.file);
5713
+ }
5678
5714
  /** 已注册 workspace 列表(项目 Tab 与写入校验共用)。 */
5679
5715
  function workspaceList$1(ctx) {
5680
5716
  return (ctx.get("workspaceRegistry")?.list?.() ?? []).filter((item) => Boolean(item) && typeof item.path === "string" && item.path !== "");
@@ -5691,10 +5727,16 @@ function requireWorkspace$2(ctx, workspacePath) {
5691
5727
  if (!workspace) throw new Error("项目未注册为 DSH workspace,不能管理项目规则");
5692
5728
  return workspace;
5693
5729
  }
5694
- /** 按作用域解析规则目录(project 先过 requireWorkspace)。 */
5695
- function dirOf$1(ctx, scope, workspacePath) {
5696
- if (scope === "project") return projectRulesDir(requireWorkspace$2(ctx, workspacePath).path);
5697
- return userRulesDir();
5730
+ /** 按作用域解析规则目录;客户端路径和未识别的来源均不作为目标使用。 */
5731
+ function dirOf$1(ctx, ref, dirs) {
5732
+ const invalid = validateRuleFile(ref.file);
5733
+ if (invalid) throw new Error(invalid);
5734
+ if (ref.scope === "project") {
5735
+ if (ref.origin !== void 0) throw new Error("项目规则不能指定用户规则来源");
5736
+ return projectRulesDir(requireWorkspace$2(ctx, ref.workspacePath).path);
5737
+ }
5738
+ if (ref.scope !== "user") throw new Error("无效的规则作用域");
5739
+ return userDirOf(ref.origin, dirs);
5698
5740
  }
5699
5741
  /**
5700
5742
  * 规则总列表:用户规则 + 各已注册工作区的项目规则。
@@ -5702,8 +5744,8 @@ function dirOf$1(ctx, scope, workspacePath) {
5702
5744
  * @param ctx DSH 上下文
5703
5745
  * @returns rules.list 载荷
5704
5746
  */
5705
- async function rulesList(ctx) {
5706
- const user = await listRulesDir(userRulesDir(), "user");
5747
+ async function rulesList(ctx, dirs = userRuleDirs()) {
5748
+ const user = await listUserRules(dirs);
5707
5749
  const projects = [];
5708
5750
  for (const workspace of workspaceList$1(ctx)) {
5709
5751
  const dir = projectRulesDir(workspace.path);
@@ -5728,8 +5770,8 @@ async function rulesList(ctx) {
5728
5770
  * @param ref 作用域 + 文件名(+ 项目工作区)
5729
5771
  * @returns 文件全文
5730
5772
  */
5731
- async function rulesRead(ctx, ref) {
5732
- const dir = dirOf$1(ctx, ref.scope, ref.workspacePath);
5773
+ async function rulesRead(ctx, ref, dirs = userRuleDirs()) {
5774
+ const dir = dirOf$1(ctx, ref, dirs);
5733
5775
  return readFile(join(dir, ref.file), "utf8");
5734
5776
  }
5735
5777
  /**
@@ -5743,12 +5785,13 @@ async function rulesRead(ctx, ref) {
5743
5785
  * @param mtime 修改时间毫秒
5744
5786
  * @returns 规则元信息
5745
5787
  */
5746
- function toRuleInfo(scope, file, absPath, parsed, size, mtime) {
5788
+ function toRuleInfo(scope, file, absPath, parsed, size, mtime, origin) {
5747
5789
  return {
5748
5790
  scope,
5749
5791
  file,
5750
5792
  absPath,
5751
- relHint: scope === "project" ? ".dsh/rules/" : "rules/",
5793
+ ...scope === "user" ? { origin: origin ?? "current" } : {},
5794
+ relHint: scope === "project" ? ".dsh/rules/" : origin === "legacy" ? "~/.dsh/rules/" : "rules/",
5752
5795
  description: parsed.description,
5753
5796
  type: ruleTypeOf(parsed),
5754
5797
  globs: parsed.globs,
@@ -5765,10 +5808,8 @@ function toRuleInfo(scope, file, absPath, parsed, size, mtime) {
5765
5808
  * @param input 保存入参
5766
5809
  * @returns 保存后的规则元信息
5767
5810
  */
5768
- async function rulesSave(ctx, input) {
5769
- const invalid = validateRuleFile(input.file);
5770
- if (invalid) throw new Error(invalid);
5771
- const dir = dirOf$1(ctx, input.scope, input.workspacePath);
5811
+ async function rulesSave(ctx, input, dirs = userRuleDirs()) {
5812
+ const dir = dirOf$1(ctx, input, dirs);
5772
5813
  await mkdir(dir, { recursive: true });
5773
5814
  const absPath = join(dir, input.file);
5774
5815
  const content = String(input.content ?? "");
@@ -5779,15 +5820,15 @@ async function rulesSave(ctx, input) {
5779
5820
  await fs.writeText(target, content, void 0, void 0, fullPolicy$2(ctx));
5780
5821
  } else await writeFile(absPath, content, "utf8");
5781
5822
  const info = await stat(absPath).catch(() => null);
5782
- return toRuleInfo(input.scope, input.file, absPath, parseRuleMdc(content), info?.size ?? Buffer$1.byteLength(content), info?.mtimeMs ?? Date.now());
5823
+ return toRuleInfo(input.scope, input.file, absPath, parseRuleMdc(content), info?.size ?? Buffer$1.byteLength(content), info?.mtimeMs ?? Date.now(), input.origin);
5783
5824
  }
5784
5825
  /**
5785
5826
  * 删除一条规则文件(project 作用域同样先过 workspace 注册校验)。
5786
5827
  * @author ddj 2026年09月03号
5787
5828
  * @param ref 作用域 + 文件名(+ 项目工作区)
5788
5829
  */
5789
- async function rulesRemove(ctx, ref) {
5790
- const dir = dirOf$1(ctx, ref.scope, ref.workspacePath);
5830
+ async function rulesRemove(ctx, ref, dirs = userRuleDirs()) {
5831
+ const dir = dirOf$1(ctx, ref, dirs);
5791
5832
  await rm(join(dir, ref.file), { force: true });
5792
5833
  }
5793
5834
  /**
@@ -5797,8 +5838,8 @@ async function rulesRemove(ctx, ref) {
5797
5838
  * @param enabled 目标状态
5798
5839
  * @returns 更新后的规则元信息
5799
5840
  */
5800
- async function rulesToggle(ctx, ref, enabled) {
5801
- const dir = dirOf$1(ctx, ref.scope, ref.workspacePath);
5841
+ async function rulesToggle(ctx, ref, enabled, dirs = userRuleDirs()) {
5842
+ const dir = dirOf$1(ctx, ref, dirs);
5802
5843
  const absPath = join(dir, ref.file);
5803
5844
  const text = await readRuleText(ctx, ref, absPath);
5804
5845
  if (text === null) throw new Error("规则文件不存在:" + ref.file);
@@ -5809,7 +5850,10 @@ async function rulesToggle(ctx, ref, enabled) {
5809
5850
  }
5810
5851
  /** 按作用域读规则全文:project 与写盘同通道走 ctx fs,user 走 node fs;缺失返回 null。 */
5811
5852
  async function readRuleText(ctx, ref, absPath) {
5812
- if (ref.scope !== "project") return readFile(absPath, "utf8").catch(() => null);
5853
+ if (ref.scope !== "project") return readFile(absPath, "utf8").catch((error) => {
5854
+ if (error.code === "ENOENT") return null;
5855
+ throw error;
5856
+ });
5813
5857
  const fs = ctx.get("fs");
5814
5858
  if (!fs) throw new Error("缺少 fs 服务");
5815
5859
  const target = await fs.resolve(".dsh/rules/" + ref.file, { cwd: requireWorkspace$2(ctx, ref.workspacePath).path });
@@ -5833,24 +5877,24 @@ async function rulesSaveContent(ctx, ref, absPath, content) {
5833
5877
  /** 重建单条规则元信息(toggle 后回传 UI;stat 失败时用内容长度 + 当前时间兜底)。 */
5834
5878
  async function ruleInfoOf(ref, absPath, content) {
5835
5879
  const info = await stat(absPath).catch(() => null);
5836
- return toRuleInfo(ref.scope, ref.file, absPath, parseRuleMdc(content), info?.size ?? Buffer$1.byteLength(content), info?.mtimeMs ?? Date.now());
5880
+ return toRuleInfo(ref.scope, ref.file, absPath, parseRuleMdc(content), info?.size ?? Buffer$1.byteLength(content), info?.mtimeMs ?? Date.now(), ref.origin);
5837
5881
  }
5838
5882
  const injCache = /* @__PURE__ */ new Map();
5839
5883
  /**
5840
- * 同步读取一个规则目录(装配 provider 内专用):仅 *.mdc,mtime 缓存,异常静默为空。
5884
+ * 同步读取一个规则目录(装配 provider 内专用):仅 *.mdc,mtime 缓存;缺失视为空。
5841
5885
  * @author ddj 2026年09月03号
5842
5886
  * @param dir 规则目录
5843
5887
  * @param scope 作用域
5844
- * @param relHint 相对提示
5888
+ * @param origin 用户规则来源
5845
5889
  * @returns 已加载规则列表
5846
5890
  */
5847
- function readRulesSync(dir, scope) {
5848
- if (!existsSync(dir)) return [];
5891
+ function readRulesSync(dir, scope, origin) {
5849
5892
  let names;
5850
5893
  try {
5851
5894
  names = readdirSync(dir);
5852
- } catch {
5853
- return [];
5895
+ } catch (error) {
5896
+ if (error.code === "ENOENT") return [];
5897
+ throw error;
5854
5898
  }
5855
5899
  const out = [];
5856
5900
  for (const file of names.filter((name) => name.endsWith(".mdc")).sort().slice(0, RULE_DIR_CAP)) {
@@ -5858,8 +5902,9 @@ function readRulesSync(dir, scope) {
5858
5902
  let info;
5859
5903
  try {
5860
5904
  info = statSync(absPath);
5861
- } catch {
5862
- continue;
5905
+ } catch (error) {
5906
+ if (error.code === "ENOENT") continue;
5907
+ throw error;
5863
5908
  }
5864
5909
  if (!info.isFile()) continue;
5865
5910
  const cached = injCache.get(absPath);
@@ -5868,8 +5913,9 @@ function readRulesSync(dir, scope) {
5868
5913
  else {
5869
5914
  try {
5870
5915
  parsed = parseRuleMdc(readFileSync(absPath, "utf8"));
5871
- } catch {
5872
- continue;
5916
+ } catch (error) {
5917
+ if (error.code === "ENOENT") continue;
5918
+ throw error;
5873
5919
  }
5874
5920
  injCache.set(absPath, {
5875
5921
  mtimeMs: info.mtimeMs,
@@ -5878,12 +5924,16 @@ function readRulesSync(dir, scope) {
5878
5924
  });
5879
5925
  }
5880
5926
  out.push({
5881
- info: toRuleInfo(scope, file, absPath, parsed, info.size, info.mtimeMs),
5927
+ info: toRuleInfo(scope, file, absPath, parsed, info.size, info.mtimeMs, origin),
5882
5928
  body: parsed.body
5883
5929
  });
5884
5930
  }
5885
5931
  return out;
5886
5932
  }
5933
+ /** 同步装配与列表同名遮蔽策略一致。 */
5934
+ function readUserRulesSync(dirs = userRuleDirs()) {
5935
+ return mergeUserRules(readRulesSync(dirs.current, "user", "current"), dirs.legacy ? readRulesSync(dirs.legacy, "user", "legacy") : [], (rule) => rule.info.file);
5936
+ }
5887
5937
  /** 从装配上下文解析会话工作区 cwd(agent.id → sessions → header.cwd;缺链返回 null)。 */
5888
5938
  function cwdFromAssemble(ctx, asm) {
5889
5939
  const id = asm?.agent?.id;
@@ -5897,12 +5947,12 @@ function cwdFromAssemble(ctx, asm) {
5897
5947
  * @param ctx DSH host 上下文
5898
5948
  * @returns 是否成功注册
5899
5949
  */
5900
- function installRulesSection(ctx) {
5950
+ function installRulesSection(ctx, dirs = userRuleDirs()) {
5901
5951
  const sp = ctx.get("systemPrompt");
5902
5952
  if (!sp || typeof sp.section !== "function") return false;
5903
5953
  const provider = (asm) => {
5904
5954
  try {
5905
- const user = readRulesSync(userRulesDir(), "user");
5955
+ const user = readUserRulesSync(dirs);
5906
5956
  const cwd = cwdFromAssemble(ctx, asm);
5907
5957
  return renderRulesSection(user, cwd ? readRulesSync(projectRulesDir(cwd), "project") : [], cwd ?? void 0);
5908
5958
  } catch {