@simonyea/holysheep-cli 2.1.37 → 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.
|
|
3
|
+
"version": "2.1.38",
|
|
4
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 && node tests/opencode-auth-purge.test.js && node tests/shell-winpath.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": [
|
package/src/tools/openclaw.js
CHANGED
|
@@ -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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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-
|
|
63
|
+
'https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep-hs26.tar.gz'
|
|
64
64
|
const DEFAULT_RUNTIME_SHA256 =
|
|
65
|
-
'
|
|
66
|
-
const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep-
|
|
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
|