@simonyea/holysheep-cli 2.1.36 → 2.1.38

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/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "2.1.36",
4
- "description": "Claude Code/Cursor/Cline API relay for China ¥1=$1, WeChat/Alipay payment, no credit card, no VPN. One command setup for all AI coding tools.",
3
+ "version": "2.1.38",
4
+ "description": "Claude Code/Cursor/Cline API relay for China \u2014 \u00a51=$1, WeChat/Alipay payment, no credit card, no VPN. One command setup for all AI coding tools.",
5
5
  "scripts": {
6
- "test": "node tests/droid.test.js && node tests/workspace-store.test.js && node tests/runtime-stale-upgrade.test.js && node tests/hermes.test.js && node tests/preflight.test.js",
6
+ "test": "node tests/droid.test.js && node tests/workspace-store.test.js && node tests/runtime-stale-upgrade.test.js && node tests/hermes.test.js && node tests/preflight.test.js && node tests/opencode-auth-purge.test.js && node tests/shell-winpath.test.js && node tests/openclaw-atomic-write.test.js",
7
7
  "prepublishOnly": "node scripts/check-tarball-size.js"
8
8
  },
9
9
  "keywords": [
@@ -25,6 +25,96 @@ const OPENCLAW_DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6'
25
25
  const OPENCLAW_DEFAULT_MINIMAX_MODEL = 'MiniMax-M2.7-highspeed'
26
26
  const OPENCLAW_PROVIDER_NAME = 'holysheep'
27
27
 
28
+ /**
29
+ * [HolySheep fork v2.1.38 / hs26] Atomic JSON write.
30
+ *
31
+ * Background: `~/.openclaw/` accumulated 30+ `openclaw.json.clobbered.*`
32
+ * backup files (mtime span hours apart, content identical) — OpenClaw's
33
+ * own config layer detected that two processes raced `writeFileSync`
34
+ * against the same path and renamed the half-written file aside to avoid
35
+ * corruption. Each race produced a backup.
36
+ *
37
+ * Root cause: `fs.writeFileSync(path, data)` is NOT atomic — it opens +
38
+ * truncates + writes; a concurrent reader/writer can observe a partial
39
+ * file or overwrite between truncation and final bytes. POSIX
40
+ * `rename(tmp, final)` IS atomic on the same filesystem, so we:
41
+ * 1. Write to `${final}.tmp.${pid}.${rand}`
42
+ * 2. `fs.renameSync(tmp, final)`
43
+ * Windows: `rename` fails if target exists → retry via copyFile + unlink.
44
+ *
45
+ * Never leaves the final path in a half-written state. If two procs race,
46
+ * one's write wins atomically and the other's wins the next one; no
47
+ * .clobbered.* files get produced by OpenClaw.
48
+ */
49
+ function atomicWriteJson(filePath, data) {
50
+ const dir = path.dirname(filePath)
51
+ fs.mkdirSync(dir, { recursive: true })
52
+ const body = JSON.stringify(data, null, 2)
53
+ const tmp = `${filePath}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 10)}`
54
+ fs.writeFileSync(tmp, body, 'utf8')
55
+ try {
56
+ fs.renameSync(tmp, filePath)
57
+ return
58
+ } catch (renameErr) {
59
+ // Windows: rename fails if target exists. Fall back to copy+unlink.
60
+ // Also handles cross-device rename (EXDEV) in edge cases.
61
+ if (process.platform === 'win32' || renameErr.code === 'EXDEV' || renameErr.code === 'EEXIST') {
62
+ try {
63
+ fs.copyFileSync(tmp, filePath)
64
+ try { fs.unlinkSync(tmp) } catch {}
65
+ return
66
+ } catch (copyErr) {
67
+ try { fs.unlinkSync(tmp) } catch {}
68
+ throw copyErr
69
+ }
70
+ }
71
+ try { fs.unlinkSync(tmp) } catch {}
72
+ throw renameErr
73
+ }
74
+ }
75
+
76
+ /**
77
+ * [HolySheep fork v2.1.38 / hs26] Prune stale OpenClaw config backup files.
78
+ *
79
+ * Removes `~/.openclaw/openclaw.json.clobbered.*` older than 7 days. These
80
+ * are produced by OpenClaw itself when a racy `writeFileSync` is detected,
81
+ * but once our atomicWriteJson is in place no new ones should appear — this
82
+ * cleanup just garbage-collects the historical accumulation without touching
83
+ * `*.last-good` / `*.bak` / `*.pre-*` which users or other tooling may need.
84
+ *
85
+ * Safe: scoped to ONLY the exact glob `openclaw.json.clobbered.*` in the
86
+ * known OpenClaw dir. No-op if dir doesn't exist or user has no matching
87
+ * files. Best-effort (each unlink wrapped in try) so a single locked file
88
+ * won't block the rest of configure().
89
+ */
90
+ function pruneClobberedBackups(maxAgeMs = 7 * 24 * 3600 * 1000) {
91
+ try {
92
+ if (!fs.existsSync(OPENCLAW_DIR)) return { scanned: 0, removed: 0 }
93
+ const entries = fs.readdirSync(OPENCLAW_DIR)
94
+ const cutoff = Date.now() - maxAgeMs
95
+ let scanned = 0
96
+ let removed = 0
97
+ for (const name of entries) {
98
+ if (!/^openclaw\.json\.clobbered\./.test(name)) continue
99
+ scanned++
100
+ const abs = path.join(OPENCLAW_DIR, name)
101
+ try {
102
+ const st = fs.statSync(abs)
103
+ if (st.mtimeMs < cutoff) {
104
+ fs.unlinkSync(abs)
105
+ removed++
106
+ }
107
+ } catch {
108
+ // locked / race — skip
109
+ }
110
+ }
111
+ return { scanned, removed }
112
+ } catch {
113
+ return { scanned: 0, removed: 0 }
114
+ }
115
+ }
116
+
117
+
28
118
  function getOpenClawBinaryCandidates() {
29
119
  return isWin ? ['openclaw.cmd', 'openclaw'] : ['openclaw']
30
120
  }
@@ -161,8 +251,8 @@ function readBridgeConfig() {
161
251
  }
162
252
 
163
253
  function writeBridgeConfig(data) {
164
- fs.mkdirSync(OPENCLAW_DIR, { recursive: true })
165
- fs.writeFileSync(BRIDGE_CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8')
254
+ // [HolySheep fork v2.1.38 / hs26] Atomic write — see atomicWriteJson.
255
+ atomicWriteJson(BRIDGE_CONFIG_FILE, data)
166
256
  }
167
257
 
168
258
  function updateBridgeConfig(patch) {
@@ -521,7 +611,9 @@ function writeManagedConfig(baseConfig, bridgeBaseUrl, apiKey, primaryModel, sel
521
611
  },
522
612
  }
523
613
 
524
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(nextConfig, null, 2), 'utf8')
614
+ // [HolySheep fork v2.1.38 / hs26] Atomic write — prevents the
615
+ // openclaw.json.clobbered.* pile-up from racing writeFileSync.
616
+ atomicWriteJson(CONFIG_FILE, nextConfig)
525
617
  return plan
526
618
  }
527
619
 
@@ -621,6 +713,17 @@ module.exports = {
621
713
  const chalk = require('chalk')
622
714
  console.log(chalk.gray('\n ⚙️ 正在配置 OpenClaw...'))
623
715
 
716
+ // [HolySheep fork v2.1.38 / hs26] Garbage-collect stale OpenClaw
717
+ // backup files from pre-atomic-write builds. Scoped to exact glob
718
+ // `openclaw.json.clobbered.*` older than 7 days — leaves .last-good
719
+ // and .bak files alone.
720
+ try {
721
+ const pruned = pruneClobberedBackups()
722
+ if (pruned.removed > 0) {
723
+ console.log(chalk.gray(` → 已清理 ${pruned.removed} 个过期的 OpenClaw 配置备份(>7 天的 .clobbered.* 文件)`))
724
+ }
725
+ } catch {}
726
+
624
727
  const runtime = detectRuntime()
625
728
  if (!runtime.available) {
626
729
  throw new Error('未检测到 OpenClaw;请先全局安装,或确保 npx 可用')
@@ -816,4 +919,7 @@ module.exports = {
816
919
  },
817
920
  installCmd: 'npm install -g openclaw@latest',
818
921
  docsUrl: 'https://docs.openclaw.ai',
922
+ // [HolySheep fork v2.1.38 / hs26] Test-only exports.
923
+ _atomicWriteJson: atomicWriteJson,
924
+ _pruneClobberedBackups: pruneClobberedBackups,
819
925
  }
@@ -60,10 +60,10 @@ const VENDOR_DIR = path.join(__dirname, 'vendor', 'aionui')
60
60
  // new CLI release, the next `hs web` invocation on user machines will detect
61
61
  // the version drift and upgrade the cache in place.
62
62
  const DEFAULT_RUNTIME_URL =
63
- 'https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep-hs25.tar.gz'
63
+ 'https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep-hs26.tar.gz'
64
64
  const DEFAULT_RUNTIME_SHA256 =
65
- '9e2a47adf8a151c9ab6916d046ee9f268aa5f1589a961d1b0374b15508ed527b'
66
- const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep-hs25'
65
+ 'e0502f21376d21b3797c51a3d932970c5a86e29076cadf3eeb2939941abb47ed'
66
+ const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep-hs26'
67
67
 
68
68
  function isValidRuntimeDir(dir) {
69
69
  if (!dir) return false
Binary file