helloloop 0.7.1 → 0.7.3
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 +21 -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 +115 -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,10 @@ 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
|
+
- 如果只是首次读取时出现瞬时异常,但重读后内容合法,则不会误生成备份文件
|
|
299
|
+
- 如果宿主自己的配置 JSON(如 `Codex marketplace.json`、`Claude settings.json`、`known_marketplaces.json`、`installed_plugins.json`)本身已损坏,`HelloLoop` 会先明确报错并停止,不会先清理现有安装再失败
|
|
296
300
|
|
|
297
301
|
### 卸载
|
|
298
302
|
|
|
@@ -426,6 +430,22 @@ npx helloloop doctor --host all --codex-home <CODEX_HOME> --claude-home <CLAUDE_
|
|
|
426
430
|
|
|
427
431
|
`HelloLoop` 只维护自己的目录和自己的注册项,不会重写别的插件条目。
|
|
428
432
|
|
|
433
|
+
## 用户目录写入范围
|
|
434
|
+
|
|
435
|
+
全局用户目录 `~/.helloloop/` 只保留一份全局设置:
|
|
436
|
+
|
|
437
|
+
```text
|
|
438
|
+
~/.helloloop/
|
|
439
|
+
└── settings.json
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
说明:
|
|
443
|
+
|
|
444
|
+
- 这里不保存项目 backlog、状态、运行记录
|
|
445
|
+
- 安装 / 升级 / 重装时,会对 `settings.json` 做结构校准,但不会校验或篡改你已存在的已知项内容
|
|
446
|
+
- 只有在 `settings.json` 被确认非法时,才会先备份,再重建为当前版本结构
|
|
447
|
+
- 如果只是读取瞬时异常、重读后合法,不会误生成 `.bak`
|
|
448
|
+
|
|
429
449
|
## `.helloloop/` 状态目录
|
|
430
450
|
|
|
431
451
|
`HelloLoop` 的 backlog、状态和运行记录始终写入目标仓库根目录下的:
|
|
@@ -441,6 +461,7 @@ npx helloloop doctor --host all --codex-home <CODEX_HOME> --claude-home <CLAUDE_
|
|
|
441
461
|
```
|
|
442
462
|
|
|
443
463
|
不会写回插件目录自身。
|
|
464
|
+
也不会写入 `~/.helloloop/`。
|
|
444
465
|
|
|
445
466
|
## 跨平台与安全
|
|
446
467
|
|
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,68 @@ 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
|
+
function tryParseUserSettingsText(text) {
|
|
152
|
+
return JSON.parse(String(text || ""));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function syncUserSettingsFile(options = {}) {
|
|
156
|
+
const settingsFile = resolveUserSettingsFile(options.userSettingsFile);
|
|
157
|
+
const defaults = defaultUserSettings();
|
|
158
|
+
|
|
159
|
+
if (!fileExists(settingsFile)) {
|
|
160
|
+
writeJson(settingsFile, defaults);
|
|
161
|
+
return {
|
|
162
|
+
settingsFile,
|
|
163
|
+
action: "created",
|
|
164
|
+
backupFile: "",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const firstText = readText(settingsFile);
|
|
169
|
+
try {
|
|
170
|
+
const parsed = tryParseUserSettingsText(firstText);
|
|
171
|
+
writeJson(settingsFile, syncUserSettingsShape(parsed));
|
|
172
|
+
return {
|
|
173
|
+
settingsFile,
|
|
174
|
+
action: "synced",
|
|
175
|
+
backupFile: "",
|
|
176
|
+
};
|
|
177
|
+
} 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
|
+
|
|
193
|
+
const backupFile = `${settingsFile}.invalid-${timestampForFile()}.bak`;
|
|
194
|
+
writeText(backupFile, retryText);
|
|
195
|
+
writeJson(settingsFile, defaults);
|
|
196
|
+
return {
|
|
197
|
+
settingsFile,
|
|
198
|
+
action: "reset_invalid_json",
|
|
199
|
+
backupFile,
|
|
200
|
+
error: String(error?.message || error || ""),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
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);
|