dsh-any-agent 0.3.0 → 0.3.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/bin/setup.mjs CHANGED
@@ -136,7 +136,7 @@ if (unknown.length) {
136
136
 
137
137
  const launch = defaultLaunch();
138
138
  const env = desiredEnv(PERMISSION);
139
- const ctx = { dirs, remove: REMOVE, write: WRITE, launch, env, byId };
139
+ const ctx = { dirs, remove: REMOVE, write: WRITE, launch, env, byId, force: FORCE };
140
140
 
141
141
  if (!JSON_OUT) {
142
142
  log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-any-agent",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Drive DeepSeek Harness (DSH) as a long-lived coding agent from any MCP client — Claude Code, Claude Desktop, Codex, Cursor, Windsurf, Zed, Gemini CLI, opencode, VS Code. Resume-aware, interruptible, approval-capable, and it only ever touches the one config entry it owns.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/clients.mjs CHANGED
@@ -18,11 +18,15 @@
18
18
  */
19
19
 
20
20
  import {
21
+ accessSync,
22
+ chmodSync,
23
+ constants as fsConstants,
21
24
  copyFileSync,
22
25
  existsSync,
23
26
  mkdirSync,
24
27
  readFileSync,
25
28
  renameSync,
29
+ statSync,
26
30
  unlinkSync,
27
31
  writeFileSync,
28
32
  } from 'node:fs';
@@ -72,6 +76,27 @@ export function atomicWrite(file, text) {
72
76
  }
73
77
  }
74
78
 
79
+ /**
80
+ * 这个文件现在能不能被我们写。
81
+ *
82
+ * 为什么不能只靠 rename 的报错:写入是「先写临时文件再 rename」,
83
+ * 而 rename 只要求**目录**可写,不要求目标文件可写。于是 POSIX 上
84
+ * 一个 0444 的配置会被静默替换掉 —— 用户明明把它设成只读,我们照写。
85
+ * Windows 上 rename 撞到只读属性会 EPERM,所以在那边看起来是好的。
86
+ * 显式查一次写权限,两个平台行为才一致。
87
+ *
88
+ * root 无视权限位,access 会返回成功 —— 那是对的,root 确实能写。
89
+ */
90
+ export function isWritable(file) {
91
+ if (!existsSync(file)) return true;
92
+ try {
93
+ accessSync(file, fsConstants.W_OK);
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
75
100
  /**
76
101
  * 读一个配置文件,把 BOM 拆出来单独记着。
77
102
  * 直接 JSON.parse 带 BOM 的文本会抛 —— 真实存在的编辑器配置里就有 BOM。
@@ -799,6 +824,38 @@ export function applyClient(plan, ctx, stamp) {
799
824
 
800
825
  if (typeof plan.next !== 'string') return plan;
801
826
 
827
+ // 只读文件不覆盖:用户把配置设成只读,通常就是不想让程序动它。
828
+ // --force 是显式的「我知道,照做」。
829
+ if (!ctx.force && !isWritable(plan.path)) {
830
+ return {
831
+ ...plan,
832
+ status: 'failed',
833
+ changed: false,
834
+ summary: 'config file is read-only, refusing to overwrite it (use --force to override)',
835
+ };
836
+ }
837
+
838
+ // 只读文件默认不覆盖:用户把它设成只读,通常就是不想让程序动它。
839
+ // --force 是显式的「我知道,照做」—— 临时去掉只读位,写完再把权限还回去,
840
+ // 不留一个被悄悄改了属性的文件。
841
+ let originalMode = null;
842
+ if (!isWritable(plan.path)) {
843
+ if (!ctx.force) {
844
+ return {
845
+ ...plan,
846
+ status: 'failed',
847
+ changed: false,
848
+ summary: 'config file is read-only, refusing to overwrite it (use --force to override)',
849
+ };
850
+ }
851
+ try {
852
+ originalMode = statSync(plan.path).mode;
853
+ chmodSync(plan.path, 0o644);
854
+ } catch (e) {
855
+ return { ...plan, status: 'failed', changed: false, summary: 'could not clear the read-only bit: ' + e.message };
856
+ }
857
+ }
858
+
802
859
  try {
803
860
  // 两个方向都备份:注册前备份,是为了「装错了能退回原样」;
804
861
  // 删除前同样备份,是为了「删错了还能装回来」。备份内容一律是操作前的原始字节。
@@ -806,10 +863,13 @@ export function applyClient(plan, ctx, stamp) {
806
863
  if (existsSync(plan.path)) backup = backupFile(plan.path, stamp);
807
864
  mkdirSync(dirname(plan.path), { recursive: true });
808
865
  atomicWrite(plan.path, plan.next);
866
+ if (originalMode !== null) {
867
+ try { chmodSync(plan.path, originalMode); } catch { /* 尽力而为 */ }
868
+ }
809
869
  const base = {
810
870
  ...plan,
811
871
  status: remove ? 'removed' : 'applied',
812
- summary: remove ? 'entry removed' : 'entry written',
872
+ summary: (remove ? 'entry removed' : 'entry written') + (originalMode !== null ? ' (read-only file overridden by --force)' : ''),
813
873
  backup,
814
874
  };
815
875
  // 有的客户端自己维护这份文件(Claude Code 会缓存并回写 ~/.claude.json)。
@@ -825,6 +885,9 @@ export function applyClient(plan, ctx, stamp) {
825
885
  }
826
886
  return base;
827
887
  } catch (e) {
888
+ if (originalMode !== null) {
889
+ try { chmodSync(plan.path, originalMode); } catch { /* 尽力而为 */ }
890
+ }
828
891
  return { ...plan, status: 'failed', changed: false, summary: 'write failed: ' + e.message };
829
892
  }
830
893
  }