Skip to content

Commit 3a486f4

Browse files
committed
fix: avoid false settings backups
1 parent 9aedc9b commit 3a486f4

7 files changed

Lines changed: 78 additions & 16 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "helloloop",
3-
"version": "0.7.2",
3+
"version": "0.7.3",
44
"description": "HelloLoop 的 Claude Code 原生插件元数据,用于多 CLI 宿主分发。",
55
"author": {
66
"name": "HelloLoop"

.codex-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "helloloop",
3-
"version": "0.7.2",
3+
"version": "0.7.3",
44
"description": "面向 Codex CLI、Claude Code、Gemini CLI 的多宿主开发工作流插件,Codex 路径为首发与参考实现。",
55
"author": {
66
"name": "HelloLoop"

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,8 @@ npx helloloop install --host all --force
294294
- `Claude` 会刷新 marketplace、缓存插件目录,以及 `settings.json` / `known_marketplaces.json` / `installed_plugins.json` 中的 `helloloop` 条目
295295
- `Gemini` 会刷新 `extensions/helloloop/`,不会动同目录下其他扩展
296296
- 安装 / 升级 / 重装时,会同步校准 `~/.helloloop/settings.json` 的当前版本结构:补齐缺失项、清理未知项、保留已知项现有值
297-
- 如果 `~/.helloloop/settings.json` 不是合法 JSON,会先备份原文件,再按当前版本结构重建
297+
- 如果 `~/.helloloop/settings.json` 被确认不是合法 JSON,会先备份原文件,再按当前版本结构重建
298+
- 如果只是首次读取时出现瞬时异常,但重读后内容合法,则不会误生成备份文件
298299
- 如果宿主自己的配置 JSON(如 `Codex marketplace.json``Claude settings.json``known_marketplaces.json``installed_plugins.json`)本身已损坏,`HelloLoop` 会先明确报错并停止,不会先清理现有安装再失败
299300

300301
### 卸载
@@ -442,7 +443,8 @@ npx helloloop doctor --host all --codex-home <CODEX_HOME> --claude-home <CLAUDE_
442443

443444
- 这里不保存项目 backlog、状态、运行记录
444445
- 安装 / 升级 / 重装时,会对 `settings.json` 做结构校准,但不会校验或篡改你已存在的已知项内容
445-
- 如果 `settings.json` 非法,会先备份,再重建为当前版本结构
446+
- 只有在 `settings.json` 被确认非法时,才会先备份,再重建为当前版本结构
447+
- 如果只是读取瞬时异常、重读后合法,不会误生成 `.bak`
446448

447449
## `.helloloop/` 状态目录
448450

hosts/gemini/extension/gemini-extension.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "helloloop",
3-
"version": "0.7.2",
3+
"version": "0.7.3",
44
"description": "HelloLoop 的 Gemini CLI 原生扩展,用于按开发文档接续推进项目开发。",
55
"contextFileName": "GEMINI.md",
66
"excludeTools": [

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "helloloop",
3-
"version": "0.7.2",
3+
"version": "0.7.3",
44
"description": "面向 Codex CLI、Claude Code、Gemini CLI 的多宿主开发工作流插件",
55
"author": "HelloLoop",
66
"license": "Apache-2.0",

src/engine_selection_settings.mjs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ export function saveUserSettings(settings, options = {}) {
148148
});
149149
}
150150

151+
function tryParseUserSettingsText(text) {
152+
return JSON.parse(String(text || ""));
153+
}
154+
151155
export function syncUserSettingsFile(options = {}) {
152156
const settingsFile = resolveUserSettingsFile(options.userSettingsFile);
153157
const defaults = defaultUserSettings();
@@ -161,12 +165,33 @@ export function syncUserSettingsFile(options = {}) {
161165
};
162166
}
163167

164-
let parsed;
168+
const firstText = readText(settingsFile);
165169
try {
166-
parsed = readJson(settingsFile);
170+
const parsed = tryParseUserSettingsText(firstText);
171+
writeJson(settingsFile, syncUserSettingsShape(parsed));
172+
return {
173+
settingsFile,
174+
action: "synced",
175+
backupFile: "",
176+
};
167177
} catch (error) {
178+
const retryText = readText(settingsFile);
179+
if (retryText !== firstText) {
180+
try {
181+
const parsed = tryParseUserSettingsText(retryText);
182+
writeJson(settingsFile, syncUserSettingsShape(parsed));
183+
return {
184+
settingsFile,
185+
action: "synced",
186+
backupFile: "",
187+
recoveredAfterRetry: true,
188+
};
189+
} catch {
190+
}
191+
}
192+
168193
const backupFile = `${settingsFile}.invalid-${timestampForFile()}.bak`;
169-
writeText(backupFile, readText(settingsFile));
194+
writeText(backupFile, retryText);
170195
writeJson(settingsFile, defaults);
171196
return {
172197
settingsFile,
@@ -175,11 +200,4 @@ export function syncUserSettingsFile(options = {}) {
175200
error: String(error?.message || error || ""),
176201
};
177202
}
178-
179-
writeJson(settingsFile, syncUserSettingsShape(parsed));
180-
return {
181-
settingsFile,
182-
action: "synced",
183-
backupFile: "",
184-
};
185203
}

tests/host_lifecycle_integrity.test.mjs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import os from "node:os";
55
import path from "node:path";
66
import { spawnSync } from "node:child_process";
77
import { fileURLToPath } from "node:url";
8+
import { syncUserSettingsFile } from "../src/engine_selection_settings.mjs";
89

910
const __filename = fileURLToPath(import.meta.url);
1011
const __dirname = path.dirname(__filename);
@@ -466,3 +467,44 @@ test("install 遇到非法 settings.json 时会备份后重建当前版本结构
466467
fs.rmSync(tempRoot, { recursive: true, force: true });
467468
}
468469
});
470+
471+
test("settings.json 在首次读取瞬间异常、重读后合法时不会产生备份文件", () => {
472+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "helloloop-settings-retry-"));
473+
const helloLoopHome = path.join(tempRoot, "helloloop-home");
474+
const settingsFile = path.join(helloLoopHome, "settings.json");
475+
const originalReadFileSync = fs.readFileSync;
476+
let firstRead = true;
477+
478+
writeJson(settingsFile, {
479+
defaultEngine: "",
480+
lastSelectedEngine: "codex",
481+
notifications: {
482+
email: {
483+
enabled: false,
484+
},
485+
},
486+
});
487+
488+
fs.readFileSync = ((filePath, ...args) => {
489+
if (String(filePath) === settingsFile && firstRead) {
490+
firstRead = false;
491+
return "{ invalid";
492+
}
493+
return originalReadFileSync.call(fs, filePath, ...args);
494+
});
495+
496+
try {
497+
const result = syncUserSettingsFile({ userSettingsFile: settingsFile });
498+
assert.equal(result.action, "synced");
499+
assert.equal(result.backupFile, "");
500+
assert.equal(result.recoveredAfterRetry, true);
501+
const backupFiles = fs.readdirSync(helloLoopHome)
502+
.filter((item) => item.startsWith("settings.json.invalid-") && item.endsWith(".bak"));
503+
assert.deepEqual(backupFiles, []);
504+
const settings = readJson(settingsFile);
505+
assert.equal(settings.lastSelectedEngine, "codex");
506+
} finally {
507+
fs.readFileSync = originalReadFileSync;
508+
fs.rmSync(tempRoot, { recursive: true, force: true });
509+
}
510+
});

0 commit comments

Comments
 (0)