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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +19 -0
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/src/cli_command_handlers.mjs +5 -0
- package/src/cli_render.mjs +16 -0
- package/src/engine_selection.mjs +0 -2
- package/src/engine_selection_settings.mjs +97 -16
- package/src/install.mjs +1 -0
- package/src/install_claude.mjs +51 -20
- package/src/install_codex.mjs +18 -14
- package/src/install_shared.mjs +11 -0
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
|
|
package/package.json
CHANGED
|
@@ -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;
|
package/src/cli_render.mjs
CHANGED
|
@@ -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`");
|
package/src/engine_selection.mjs
CHANGED
|
@@ -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
|
|
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 =
|
|
139
|
+
const currentSettings = readRawUserSettingsDocument(options);
|
|
140
|
+
const mergedSettings = mergeValueBySchema(
|
|
141
|
+
defaultUserSettings(),
|
|
142
|
+
currentSettings,
|
|
143
|
+
settings,
|
|
144
|
+
);
|
|
145
|
+
|
|
91
146
|
writeJson(resolveUserSettingsFile(options.userSettingsFile), {
|
|
92
|
-
...
|
|
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
package/src/install_claude.mjs
CHANGED
|
@@ -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(
|
|
54
|
-
|
|
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(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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(
|
|
149
|
-
|
|
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",
|
package/src/install_codex.mjs
CHANGED
|
@@ -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 =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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",
|
package/src/install_shared.mjs
CHANGED
|
@@ -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);
|