helloloop 0.7.1 → 0.7.2

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloloop",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "HelloLoop 的 Claude Code 原生插件元数据,用于多 CLI 宿主分发。",
5
5
  "author": {
6
6
  "name": "HelloLoop"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloloop",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "面向 Codex CLI、Claude Code、Gemini CLI 的多宿主开发工作流插件,Codex 路径为首发与参考实现。",
5
5
  "author": {
6
6
  "name": "HelloLoop"
package/README.md CHANGED
@@ -293,6 +293,9 @@ npx helloloop install --host all --force
293
293
  - `Codex` 会刷新插件目录和 marketplace 条目
294
294
  - `Claude` 会刷新 marketplace、缓存插件目录,以及 `settings.json` / `known_marketplaces.json` / `installed_plugins.json` 中的 `helloloop` 条目
295
295
  - `Gemini` 会刷新 `extensions/helloloop/`,不会动同目录下其他扩展
296
+ - 安装 / 升级 / 重装时,会同步校准 `~/.helloloop/settings.json` 的当前版本结构:补齐缺失项、清理未知项、保留已知项现有值
297
+ - 如果 `~/.helloloop/settings.json` 不是合法 JSON,会先备份原文件,再按当前版本结构重建
298
+ - 如果宿主自己的配置 JSON(如 `Codex marketplace.json`、`Claude settings.json`、`known_marketplaces.json`、`installed_plugins.json`)本身已损坏,`HelloLoop` 会先明确报错并停止,不会先清理现有安装再失败
296
299
 
297
300
  ### 卸载
298
301
 
@@ -426,6 +429,21 @@ npx helloloop doctor --host all --codex-home <CODEX_HOME> --claude-home <CLAUDE_
426
429
 
427
430
  `HelloLoop` 只维护自己的目录和自己的注册项,不会重写别的插件条目。
428
431
 
432
+ ## 用户目录写入范围
433
+
434
+ 全局用户目录 `~/.helloloop/` 只保留一份全局设置:
435
+
436
+ ```text
437
+ ~/.helloloop/
438
+ └── settings.json
439
+ ```
440
+
441
+ 说明:
442
+
443
+ - 这里不保存项目 backlog、状态、运行记录
444
+ - 安装 / 升级 / 重装时,会对 `settings.json` 做结构校准,但不会校验或篡改你已存在的已知项内容
445
+ - 如果 `settings.json` 非法,会先备份,再重建为当前版本结构
446
+
429
447
  ## `.helloloop/` 状态目录
430
448
 
431
449
  `HelloLoop` 的 backlog、状态和运行记录始终写入目标仓库根目录下的:
@@ -441,6 +459,7 @@ npx helloloop doctor --host all --codex-home <CODEX_HOME> --claude-home <CLAUDE_
441
459
  ```
442
460
 
443
461
  不会写回插件目录自身。
462
+ 也不会写入 `~/.helloloop/`。
444
463
 
445
464
  ## 跨平台与安全
446
465
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloloop",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "HelloLoop 的 Gemini CLI 原生扩展,用于按开发文档接续推进项目开发。",
5
5
  "contextFileName": "GEMINI.md",
6
6
  "excludeTools": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloloop",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "面向 Codex CLI、Claude Code、Gemini CLI 的多宿主开发工作流插件",
5
5
  "author": "HelloLoop",
6
6
  "license": "Apache-2.0",
@@ -2,11 +2,15 @@ import path from "node:path";
2
2
 
3
3
  import { createContext } from "./context.mjs";
4
4
  import { loadBacklog, scaffoldIfMissing } from "./config.mjs";
5
+ import { syncUserSettingsFile } from "./engine_selection_settings.mjs";
5
6
  import { installPluginBundle, uninstallPluginBundle } from "./install.mjs";
6
7
  import { runLoop, runOnce, renderStatusText } from "./runner.mjs";
7
8
  import { renderInstallSummary, renderUninstallSummary } from "./cli_render.mjs";
8
9
 
9
10
  export function handleInstallCommand(options) {
11
+ const userSettings = syncUserSettingsFile({
12
+ userSettingsFile: options.userSettingsFile,
13
+ });
10
14
  const context = createContext({
11
15
  repoRoot: options.repoRoot,
12
16
  configDirName: options.configDirName,
@@ -18,6 +22,7 @@ export function handleInstallCommand(options) {
18
22
  claudeHome: options.claudeHome,
19
23
  geminiHome: options.geminiHome,
20
24
  force: options.force,
25
+ userSettings,
21
26
  });
22
27
  console.log(renderInstallSummary(result));
23
28
  return 0;
@@ -29,6 +29,22 @@ export function renderInstallSummary(result) {
29
29
  }
30
30
  }
31
31
 
32
+ if (result.userSettings?.settingsFile) {
33
+ lines.push("");
34
+ lines.push("全局设置:");
35
+ lines.push(`- 配置文件:${result.userSettings.settingsFile}`);
36
+ if (result.userSettings.action === "created") {
37
+ lines.push("- 结构校准:已创建默认 settings.json");
38
+ } else if (result.userSettings.action === "reset_invalid_json") {
39
+ lines.push("- 结构校准:检测到非法 JSON,已重建为当前版本结构");
40
+ if (result.userSettings.backupFile) {
41
+ lines.push(`- 备份文件:${result.userSettings.backupFile}`);
42
+ }
43
+ } else {
44
+ lines.push("- 结构校准:已按当前版本同步 settings.json 项");
45
+ }
46
+ }
47
+
32
48
  lines.push("");
33
49
  lines.push("使用入口:");
34
50
  lines.push("- Codex:`$helloloop` / `npx helloloop`");
@@ -327,9 +327,7 @@ export function rememberEngineSelection(context, engineResolution, options = {})
327
327
  });
328
328
  }
329
329
 
330
- const userSettings = loadUserSettings(options);
331
330
  saveUserSettings({
332
- ...userSettings,
333
331
  lastSelectedEngine: engine,
334
332
  }, options);
335
333
  }
@@ -1,7 +1,7 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
3
 
4
- import { fileExists, readJson, writeJson } from "./common.mjs";
4
+ import { fileExists, readJson, readText, timestampForFile, writeJson, writeText } from "./common.mjs";
5
5
  import { normalizeEngineName } from "./engine_metadata.mjs";
6
6
 
7
7
  function defaultEmailNotificationSettings() {
@@ -34,6 +34,57 @@ function defaultUserSettings() {
34
34
  };
35
35
  }
36
36
 
37
+ function cloneJsonValue(value) {
38
+ return JSON.parse(JSON.stringify(value));
39
+ }
40
+
41
+ function isPlainObject(value) {
42
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
43
+ }
44
+
45
+ function syncValueBySchema(schemaValue, currentValue) {
46
+ if (!isPlainObject(schemaValue)) {
47
+ return currentValue === undefined ? cloneJsonValue(schemaValue) : currentValue;
48
+ }
49
+
50
+ const source = isPlainObject(currentValue) ? currentValue : {};
51
+ const next = {};
52
+ for (const [key, childSchema] of Object.entries(schemaValue)) {
53
+ next[key] = syncValueBySchema(childSchema, Object.hasOwn(source, key) ? source[key] : undefined);
54
+ }
55
+ return next;
56
+ }
57
+
58
+ function mergeValueBySchema(schemaValue, baseValue, patchValue) {
59
+ if (!isPlainObject(schemaValue)) {
60
+ return patchValue === undefined ? baseValue : patchValue;
61
+ }
62
+
63
+ const baseObject = isPlainObject(baseValue) ? baseValue : {};
64
+ const patchObject = isPlainObject(patchValue) ? patchValue : {};
65
+ const next = {};
66
+ for (const [key, childSchema] of Object.entries(schemaValue)) {
67
+ const nextBaseValue = Object.hasOwn(baseObject, key) ? baseObject[key] : undefined;
68
+ const hasPatchedKey = isPlainObject(patchValue) && Object.hasOwn(patchObject, key);
69
+ next[key] = mergeValueBySchema(
70
+ childSchema,
71
+ nextBaseValue,
72
+ hasPatchedKey ? patchObject[key] : undefined,
73
+ );
74
+ }
75
+ return next;
76
+ }
77
+
78
+ export function syncUserSettingsShape(settings = {}) {
79
+ return syncValueBySchema(defaultUserSettings(), settings);
80
+ }
81
+
82
+ function readRawUserSettingsDocument(options = {}) {
83
+ const settingsFile = resolveUserSettingsFile(options.userSettingsFile);
84
+ const settings = fileExists(settingsFile) ? readJson(settingsFile) : {};
85
+ return syncUserSettingsShape(settings);
86
+ }
87
+
37
88
  export function resolveUserSettingsHome() {
38
89
  return String(process.env.HELLOLOOP_HOME || "").trim()
39
90
  || path.join(os.homedir(), ".helloloop");
@@ -63,11 +114,9 @@ function normalizeEmailNotificationSettings(emailSettings = {}) {
63
114
  }
64
115
 
65
116
  export function loadUserSettingsDocument(options = {}) {
66
- const settingsFile = resolveUserSettingsFile(options.userSettingsFile);
67
- const settings = fileExists(settingsFile) ? readJson(settingsFile) : {};
117
+ const settings = readRawUserSettingsDocument(options);
68
118
 
69
119
  return {
70
- ...defaultUserSettings(),
71
120
  ...settings,
72
121
  defaultEngine: normalizeEngineName(settings?.defaultEngine),
73
122
  lastSelectedEngine: normalizeEngineName(settings?.lastSelectedEngine),
@@ -87,18 +136,50 @@ export function loadUserSettings(options = {}) {
87
136
  }
88
137
 
89
138
  export function saveUserSettings(settings, options = {}) {
90
- const currentSettings = loadUserSettingsDocument(options);
139
+ const currentSettings = readRawUserSettingsDocument(options);
140
+ const mergedSettings = mergeValueBySchema(
141
+ defaultUserSettings(),
142
+ currentSettings,
143
+ settings,
144
+ );
145
+
91
146
  writeJson(resolveUserSettingsFile(options.userSettingsFile), {
92
- ...currentSettings,
93
- ...settings,
94
- defaultEngine: normalizeEngineName(settings?.defaultEngine ?? currentSettings.defaultEngine),
95
- lastSelectedEngine: normalizeEngineName(settings?.lastSelectedEngine ?? currentSettings.lastSelectedEngine),
96
- notifications: {
97
- ...(currentSettings.notifications || {}),
98
- ...(settings?.notifications || {}),
99
- email: normalizeEmailNotificationSettings(
100
- settings?.notifications?.email ?? currentSettings.notifications?.email ?? {},
101
- ),
102
- },
147
+ ...syncUserSettingsShape(mergedSettings),
103
148
  });
104
149
  }
150
+
151
+ export function syncUserSettingsFile(options = {}) {
152
+ const settingsFile = resolveUserSettingsFile(options.userSettingsFile);
153
+ const defaults = defaultUserSettings();
154
+
155
+ if (!fileExists(settingsFile)) {
156
+ writeJson(settingsFile, defaults);
157
+ return {
158
+ settingsFile,
159
+ action: "created",
160
+ backupFile: "",
161
+ };
162
+ }
163
+
164
+ let parsed;
165
+ try {
166
+ parsed = readJson(settingsFile);
167
+ } catch (error) {
168
+ const backupFile = `${settingsFile}.invalid-${timestampForFile()}.bak`;
169
+ writeText(backupFile, readText(settingsFile));
170
+ writeJson(settingsFile, defaults);
171
+ return {
172
+ settingsFile,
173
+ action: "reset_invalid_json",
174
+ backupFile,
175
+ error: String(error?.message || error || ""),
176
+ };
177
+ }
178
+
179
+ writeJson(settingsFile, syncUserSettingsShape(parsed));
180
+ return {
181
+ settingsFile,
182
+ action: "synced",
183
+ backupFile: "",
184
+ };
185
+ }
package/src/install.mjs CHANGED
@@ -38,6 +38,7 @@ export function installPluginBundle(options = {}) {
38
38
  installedHosts,
39
39
  targetPluginRoot: codexResult?.targetRoot || "",
40
40
  marketplaceFile: codexResult?.marketplaceFile || "",
41
+ userSettings: options.userSettings || null,
41
42
  };
42
43
  }
43
44
 
@@ -6,13 +6,14 @@ import {
6
6
  CLAUDE_PLUGIN_KEY,
7
7
  assertPathInside,
8
8
  copyDirectory,
9
+ readExistingJsonOrThrow,
9
10
  removePathIfExists,
10
11
  removeTargetIfNeeded,
11
12
  resolveHomeDir,
12
13
  } from "./install_shared.mjs";
13
14
 
14
- function updateClaudeSettings(settingsFile, marketplaceRoot) {
15
- const settings = fileExists(settingsFile) ? readJson(settingsFile) : {};
15
+ function updateClaudeSettings(settingsFile, marketplaceRoot, existingSettings = null) {
16
+ const settings = existingSettings || (fileExists(settingsFile) ? readJson(settingsFile) : {});
16
17
 
17
18
  settings.extraKnownMarketplaces = settings.extraKnownMarketplaces || {};
18
19
  settings.enabledPlugins = settings.enabledPlugins || {};
@@ -27,12 +28,12 @@ function updateClaudeSettings(settingsFile, marketplaceRoot) {
27
28
  writeJson(settingsFile, settings);
28
29
  }
29
30
 
30
- function removeClaudeSettingsEntries(settingsFile) {
31
+ function removeClaudeSettingsEntries(settingsFile, existingSettings = null) {
31
32
  if (!fileExists(settingsFile)) {
32
33
  return false;
33
34
  }
34
35
 
35
- const settings = readJson(settingsFile);
36
+ const settings = existingSettings || readJson(settingsFile);
36
37
  let changed = false;
37
38
 
38
39
  if (settings.extraKnownMarketplaces && Object.hasOwn(settings.extraKnownMarketplaces, CLAUDE_MARKETPLACE_NAME)) {
@@ -50,8 +51,13 @@ function removeClaudeSettingsEntries(settingsFile) {
50
51
  return changed;
51
52
  }
52
53
 
53
- function updateClaudeKnownMarketplaces(knownMarketplacesFile, marketplaceRoot, updatedAt) {
54
- const knownMarketplaces = fileExists(knownMarketplacesFile) ? readJson(knownMarketplacesFile) : {};
54
+ function updateClaudeKnownMarketplaces(
55
+ knownMarketplacesFile,
56
+ marketplaceRoot,
57
+ updatedAt,
58
+ existingKnownMarketplaces = null,
59
+ ) {
60
+ const knownMarketplaces = existingKnownMarketplaces || (fileExists(knownMarketplacesFile) ? readJson(knownMarketplacesFile) : {});
55
61
  knownMarketplaces[CLAUDE_MARKETPLACE_NAME] = {
56
62
  source: {
57
63
  source: "directory",
@@ -63,12 +69,12 @@ function updateClaudeKnownMarketplaces(knownMarketplacesFile, marketplaceRoot, u
63
69
  writeJson(knownMarketplacesFile, knownMarketplaces);
64
70
  }
65
71
 
66
- function removeClaudeKnownMarketplace(knownMarketplacesFile) {
72
+ function removeClaudeKnownMarketplace(knownMarketplacesFile, existingKnownMarketplaces = null) {
67
73
  if (!fileExists(knownMarketplacesFile)) {
68
74
  return false;
69
75
  }
70
76
 
71
- const knownMarketplaces = readJson(knownMarketplacesFile);
77
+ const knownMarketplaces = existingKnownMarketplaces || readJson(knownMarketplacesFile);
72
78
  if (!Object.hasOwn(knownMarketplaces, CLAUDE_MARKETPLACE_NAME)) {
73
79
  return false;
74
80
  }
@@ -78,10 +84,18 @@ function removeClaudeKnownMarketplace(knownMarketplacesFile) {
78
84
  return true;
79
85
  }
80
86
 
81
- function updateClaudeInstalledPlugins(installedPluginsFile, pluginRoot, pluginVersion, updatedAt) {
82
- const installedPlugins = fileExists(installedPluginsFile)
83
- ? readJson(installedPluginsFile)
84
- : { version: 2, plugins: {} };
87
+ function updateClaudeInstalledPlugins(
88
+ installedPluginsFile,
89
+ pluginRoot,
90
+ pluginVersion,
91
+ updatedAt,
92
+ existingInstalledPlugins = null,
93
+ ) {
94
+ const installedPlugins = existingInstalledPlugins
95
+ ? existingInstalledPlugins
96
+ : (fileExists(installedPluginsFile)
97
+ ? readJson(installedPluginsFile)
98
+ : { version: 2, plugins: {} });
85
99
 
86
100
  installedPlugins.version = 2;
87
101
  installedPlugins.plugins = installedPlugins.plugins || {};
@@ -98,12 +112,12 @@ function updateClaudeInstalledPlugins(installedPluginsFile, pluginRoot, pluginVe
98
112
  writeJson(installedPluginsFile, installedPlugins);
99
113
  }
100
114
 
101
- function removeClaudeInstalledPlugin(installedPluginsFile) {
115
+ function removeClaudeInstalledPlugin(installedPluginsFile, existingInstalledPlugins = null) {
102
116
  if (!fileExists(installedPluginsFile)) {
103
117
  return false;
104
118
  }
105
119
 
106
- const installedPlugins = readJson(installedPluginsFile);
120
+ const installedPlugins = existingInstalledPlugins || readJson(installedPluginsFile);
107
121
  if (!installedPlugins.plugins || !Object.hasOwn(installedPlugins.plugins, CLAUDE_PLUGIN_KEY)) {
108
122
  return false;
109
123
  }
@@ -125,6 +139,9 @@ export function installClaudeHost(bundleRoot, options = {}) {
125
139
  const knownMarketplacesFile = path.join(targetPluginsRoot, "known_marketplaces.json");
126
140
  const installedPluginsFile = path.join(targetPluginsRoot, "installed_plugins.json");
127
141
  const settingsFile = path.join(resolvedClaudeHome, "settings.json");
142
+ const existingSettings = readExistingJsonOrThrow(settingsFile, "Claude settings 配置");
143
+ const existingKnownMarketplaces = readExistingJsonOrThrow(knownMarketplacesFile, "Claude known_marketplaces 配置");
144
+ const existingInstalledPlugins = readExistingJsonOrThrow(installedPluginsFile, "Claude installed_plugins 配置");
128
145
 
129
146
  if (!fileExists(sourceManifest)) {
130
147
  throw new Error(`未找到 Claude 插件 manifest:${sourceManifest}`);
@@ -144,9 +161,20 @@ export function installClaudeHost(bundleRoot, options = {}) {
144
161
  copyDirectory(sourceMarketplaceRoot, targetMarketplaceRoot);
145
162
  copyDirectory(path.join(sourceMarketplaceRoot, "plugins", "helloloop"), targetInstalledPluginRoot);
146
163
  const updatedAt = nowIso();
147
- updateClaudeSettings(settingsFile, targetMarketplaceRoot);
148
- updateClaudeKnownMarketplaces(knownMarketplacesFile, targetMarketplaceRoot, updatedAt);
149
- updateClaudeInstalledPlugins(installedPluginsFile, targetInstalledPluginRoot, pluginVersion, updatedAt);
164
+ updateClaudeSettings(settingsFile, targetMarketplaceRoot, existingSettings);
165
+ updateClaudeKnownMarketplaces(
166
+ knownMarketplacesFile,
167
+ targetMarketplaceRoot,
168
+ updatedAt,
169
+ existingKnownMarketplaces,
170
+ );
171
+ updateClaudeInstalledPlugins(
172
+ installedPluginsFile,
173
+ targetInstalledPluginRoot,
174
+ pluginVersion,
175
+ updatedAt,
176
+ existingInstalledPlugins,
177
+ );
150
178
 
151
179
  return {
152
180
  host: "claude",
@@ -165,12 +193,15 @@ export function uninstallClaudeHost(options = {}) {
165
193
  const knownMarketplacesFile = path.join(targetPluginsRoot, "known_marketplaces.json");
166
194
  const installedPluginsFile = path.join(targetPluginsRoot, "installed_plugins.json");
167
195
  const settingsFile = path.join(resolvedClaudeHome, "settings.json");
196
+ const existingSettings = readExistingJsonOrThrow(settingsFile, "Claude settings 配置");
197
+ const existingKnownMarketplaces = readExistingJsonOrThrow(knownMarketplacesFile, "Claude known_marketplaces 配置");
198
+ const existingInstalledPlugins = readExistingJsonOrThrow(installedPluginsFile, "Claude installed_plugins 配置");
168
199
 
169
200
  const removedMarketplaceDir = removePathIfExists(targetMarketplaceRoot);
170
201
  const removedCacheDir = removePathIfExists(targetCachePluginsRoot);
171
- const removedKnownMarketplace = removeClaudeKnownMarketplace(knownMarketplacesFile);
172
- const removedInstalledPlugin = removeClaudeInstalledPlugin(installedPluginsFile);
173
- const removedSettingsEntries = removeClaudeSettingsEntries(settingsFile);
202
+ const removedKnownMarketplace = removeClaudeKnownMarketplace(knownMarketplacesFile, existingKnownMarketplaces);
203
+ const removedInstalledPlugin = removeClaudeInstalledPlugin(installedPluginsFile, existingInstalledPlugins);
204
+ const removedSettingsEntries = removeClaudeSettingsEntries(settingsFile, existingSettings);
174
205
 
175
206
  return {
176
207
  host: "claude",
@@ -5,21 +5,23 @@ import {
5
5
  assertPathInside,
6
6
  codexBundleEntries,
7
7
  copyBundleEntries,
8
+ readExistingJsonOrThrow,
8
9
  removePathIfExists,
9
10
  removeTargetIfNeeded,
10
11
  resolveHomeDir,
11
12
  } from "./install_shared.mjs";
12
13
 
13
- function updateCodexMarketplace(marketplaceFile) {
14
- const marketplace = fileExists(marketplaceFile)
15
- ? readJson(marketplaceFile)
16
- : {
17
- name: "local-plugins",
18
- interface: {
19
- displayName: "Local Plugins",
20
- },
21
- plugins: [],
22
- };
14
+ function updateCodexMarketplace(marketplaceFile, existingMarketplace = null) {
15
+ const marketplace = existingMarketplace
16
+ || (fileExists(marketplaceFile)
17
+ ? readJson(marketplaceFile)
18
+ : {
19
+ name: "local-plugins",
20
+ interface: {
21
+ displayName: "Local Plugins",
22
+ },
23
+ plugins: [],
24
+ });
23
25
 
24
26
  marketplace.interface = marketplace.interface || {
25
27
  displayName: "Local Plugins",
@@ -49,12 +51,12 @@ function updateCodexMarketplace(marketplaceFile) {
49
51
  writeJson(marketplaceFile, marketplace);
50
52
  }
51
53
 
52
- function removeCodexMarketplaceEntry(marketplaceFile) {
54
+ function removeCodexMarketplaceEntry(marketplaceFile, existingMarketplace = null) {
53
55
  if (!fileExists(marketplaceFile)) {
54
56
  return false;
55
57
  }
56
58
 
57
- const marketplace = readJson(marketplaceFile);
59
+ const marketplace = existingMarketplace || readJson(marketplaceFile);
58
60
  const plugins = Array.isArray(marketplace.plugins) ? marketplace.plugins : [];
59
61
  const nextPlugins = plugins.filter((plugin) => plugin?.name !== "helloloop");
60
62
  if (nextPlugins.length === plugins.length) {
@@ -72,6 +74,7 @@ export function installCodexHost(bundleRoot, options = {}) {
72
74
  const targetPluginRoot = path.join(targetPluginsRoot, "helloloop");
73
75
  const marketplaceFile = path.join(resolvedCodexHome, ".agents", "plugins", "marketplace.json");
74
76
  const manifestFile = path.join(bundleRoot, ".codex-plugin", "plugin.json");
77
+ const existingMarketplace = readExistingJsonOrThrow(marketplaceFile, "Codex marketplace 配置");
75
78
 
76
79
  if (!fileExists(manifestFile)) {
77
80
  throw new Error(`未找到 Codex 插件 manifest:${manifestFile}`);
@@ -86,7 +89,7 @@ export function installCodexHost(bundleRoot, options = {}) {
86
89
  removePathIfExists(path.join(targetPluginRoot, ".git"));
87
90
 
88
91
  ensureDir(path.dirname(marketplaceFile));
89
- updateCodexMarketplace(marketplaceFile);
92
+ updateCodexMarketplace(marketplaceFile, existingMarketplace);
90
93
 
91
94
  return {
92
95
  host: "codex",
@@ -100,9 +103,10 @@ export function uninstallCodexHost(options = {}) {
100
103
  const resolvedCodexHome = resolveHomeDir(options.codexHome, ".codex");
101
104
  const targetPluginRoot = path.join(resolvedCodexHome, "plugins", "helloloop");
102
105
  const marketplaceFile = path.join(resolvedCodexHome, ".agents", "plugins", "marketplace.json");
106
+ const existingMarketplace = readExistingJsonOrThrow(marketplaceFile, "Codex marketplace 配置");
103
107
 
104
108
  const removedPlugin = removePathIfExists(targetPluginRoot);
105
- const removedMarketplace = removeCodexMarketplaceEntry(marketplaceFile);
109
+ const removedMarketplace = removeCodexMarketplaceEntry(marketplaceFile, existingMarketplace);
106
110
 
107
111
  return {
108
112
  host: "codex",
@@ -132,6 +132,17 @@ export function loadOrInitJson(filePath, fallbackValue) {
132
132
  return readJson(filePath);
133
133
  }
134
134
 
135
+ export function readExistingJsonOrThrow(filePath, label) {
136
+ if (!fileExists(filePath)) {
137
+ return null;
138
+ }
139
+ try {
140
+ return readJson(filePath);
141
+ } catch (error) {
142
+ throw new Error(`${label} 不是合法 JSON:${filePath}`);
143
+ }
144
+ }
145
+
135
146
  export function writeJsonFile(filePath, value) {
136
147
  ensureDir(path.dirname(filePath));
137
148
  writeJson(filePath, value);