helloloop 0.7.0 → 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 +3 -3
- package/README.md +20 -0
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/skills/helloloop/SKILL.md +3 -2
- package/src/cli_command_handlers.mjs +5 -0
- package/src/cli_render.mjs +16 -0
- package/src/discovery_paths.mjs +65 -1
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helloloop",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "面向 Codex CLI、Claude Code、Gemini CLI 的多宿主开发工作流插件,Codex 路径为首发与参考实现。",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HelloLoop"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"interface": {
|
|
18
18
|
"displayName": "HelloLoop",
|
|
19
19
|
"shortDescription": "显式调用后按 backlog 持续开发与验证",
|
|
20
|
-
"longDescription": "HelloLoop 把基于 backlog 的持续开发能力封装为官方 Codex 插件 bundle。只有在用户显式调用 helloloop skill
|
|
20
|
+
"longDescription": "HelloLoop 把基于 backlog 的持续开发能力封装为官方 Codex 插件 bundle。只有在用户显式调用 helloloop skill 时才应介入;进入后默认优先走 npx helloloop 主 CLI 流程:支持 npx helloloop、npx helloloop <PATH>、npx helloloop codex|claude|gemini ...,先分析、再展示确认单、确认后自动接续推进;运行中按无人值守自动恢复持续推进主线。",
|
|
21
21
|
"developerName": "HelloLoop",
|
|
22
22
|
"category": "Coding",
|
|
23
23
|
"capabilities": [
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"Write"
|
|
26
26
|
],
|
|
27
27
|
"defaultPrompt": [
|
|
28
|
-
"只有当用户显式调用 $helloloop / #helloloop / helloloop:helloloop
|
|
28
|
+
"只有当用户显式调用 $helloloop / #helloloop / helloloop:helloloop 时,才进入 npx helloloop 主 CLI 流程;普通 Codex 会话不要自动接管。仅仅提到 helloloop 仓库、插件名、README、代码、测试、issue、release、npm 包名,都不算调用。进入后优先执行 npx helloloop 或 npx helloloop <PATH>;如果用户明确指定执行引擎,也允许使用 npx helloloop codex|claude|gemini ...。启动前确认一次,启动后按无人值守自动恢复与主线续跑执行,不要在对话里手工模拟流程。"
|
|
29
29
|
],
|
|
30
30
|
"brandColor": "#0F766E"
|
|
31
31
|
}
|
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
这意味着:
|
|
20
20
|
|
|
21
21
|
- 三宿主都只在用户显式调用 `helloloop` 时介入;普通会话不会被 HelloLoop 自动接管
|
|
22
|
+
- 在 `Codex` 中,只有显式输入 `$helloloop` / `#helloloop` / `helloloop:helloloop` 才算调用;仅仅提到 `helloloop` 仓库、README、代码、测试、release、npm 包名,都不算调用
|
|
22
23
|
- 无论在终端还是在 `Codex` / `Claude` / `Gemini` 宿主内,只要用户未明确指定引擎,`HelloLoop` 都会先询问本轮执行引擎
|
|
23
24
|
- 当前宿主、项目历史、用户历史只作为推荐依据,不会自动替你选中引擎
|
|
24
25
|
- 如果你已经显式指定,或已经在首轮确认中明确选定了引擎,本轮就固定按该引擎执行
|
|
@@ -292,6 +293,9 @@ npx helloloop install --host all --force
|
|
|
292
293
|
- `Codex` 会刷新插件目录和 marketplace 条目
|
|
293
294
|
- `Claude` 会刷新 marketplace、缓存插件目录,以及 `settings.json` / `known_marketplaces.json` / `installed_plugins.json` 中的 `helloloop` 条目
|
|
294
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` 会先明确报错并停止,不会先清理现有安装再失败
|
|
295
299
|
|
|
296
300
|
### 卸载
|
|
297
301
|
|
|
@@ -425,6 +429,21 @@ npx helloloop doctor --host all --codex-home <CODEX_HOME> --claude-home <CLAUDE_
|
|
|
425
429
|
|
|
426
430
|
`HelloLoop` 只维护自己的目录和自己的注册项,不会重写别的插件条目。
|
|
427
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
|
+
|
|
428
447
|
## `.helloloop/` 状态目录
|
|
429
448
|
|
|
430
449
|
`HelloLoop` 的 backlog、状态和运行记录始终写入目标仓库根目录下的:
|
|
@@ -440,6 +459,7 @@ npx helloloop doctor --host all --codex-home <CODEX_HOME> --claude-home <CLAUDE_
|
|
|
440
459
|
```
|
|
441
460
|
|
|
442
461
|
不会写回插件目录自身。
|
|
462
|
+
也不会写入 `~/.helloloop/`。
|
|
443
463
|
|
|
444
464
|
## 跨平台与安全
|
|
445
465
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: helloloop
|
|
3
|
-
description: 仅当用户显式调用 `$helloloop` / `#helloloop` / `helloloop:helloloop
|
|
3
|
+
description: 仅当用户显式调用 `$helloloop` / `#helloloop` / `helloloop:helloloop` 时使用。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# HelloLoop
|
|
@@ -9,7 +9,8 @@ description: 仅当用户显式调用 `$helloloop` / `#helloloop` / `helloloop:h
|
|
|
9
9
|
|
|
10
10
|
## 强制入口规则
|
|
11
11
|
|
|
12
|
-
-
|
|
12
|
+
- 只有用户显式调用 `$helloloop` / `#helloloop` / `helloloop:helloloop` 时,才允许进入 HelloLoop;未显式调用时,不允许接管普通 Codex 会话。
|
|
13
|
+
- 仅仅提到 `helloloop` 仓库、插件名、README、代码、测试、issue、release、npm 包名,都不算调用 HelloLoop。
|
|
13
14
|
- 用户显式调用 `$helloloop` / `#helloloop` / `helloloop:helloloop` 时,默认必须优先执行 `npx helloloop` 或 `npx helloloop <PATH>`;如果用户又明确指定了执行引擎,也允许使用 `npx helloloop codex|claude|gemini ...`。
|
|
14
15
|
- 用户没有明确指定执行引擎时,不允许由 skill 自行补成 `codex` / `claude` / `gemini`;必须让 `HelloLoop` 先完成引擎确认。
|
|
15
16
|
- 不允许在对话里手工模拟 `HelloLoop` 的分析、确认单、backlog 编排和自动续跑流程来代替 CLI。
|
|
@@ -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/discovery_paths.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
|
|
4
5
|
import { fileExists } from "./common.mjs";
|
|
@@ -59,6 +60,26 @@ const IGNORED_PROJECT_SEGMENTS = new Set([
|
|
|
59
60
|
"venv",
|
|
60
61
|
]);
|
|
61
62
|
|
|
63
|
+
const PREFERRED_ANCESTOR_SEGMENTS = new Set([
|
|
64
|
+
".cache",
|
|
65
|
+
".next",
|
|
66
|
+
".nuxt",
|
|
67
|
+
".pnpm",
|
|
68
|
+
".turbo",
|
|
69
|
+
".venv",
|
|
70
|
+
".yarn",
|
|
71
|
+
"__pycache__",
|
|
72
|
+
"build",
|
|
73
|
+
"cache",
|
|
74
|
+
"coverage",
|
|
75
|
+
"dist",
|
|
76
|
+
"node_modules",
|
|
77
|
+
"out",
|
|
78
|
+
"target",
|
|
79
|
+
"vendor",
|
|
80
|
+
"venv",
|
|
81
|
+
]);
|
|
82
|
+
|
|
62
83
|
function listImmediateDirectories(directoryPath) {
|
|
63
84
|
if (!pathExists(directoryPath) || !fs.statSync(directoryPath).isDirectory()) {
|
|
64
85
|
return [];
|
|
@@ -99,13 +120,52 @@ function hasIgnoredProjectBasename(targetPath) {
|
|
|
99
120
|
return IGNORED_PROJECT_SEGMENTS.has(path.basename(targetPath).toLowerCase());
|
|
100
121
|
}
|
|
101
122
|
|
|
123
|
+
function canonicalPath(targetPath) {
|
|
124
|
+
if (!targetPath) {
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const resolved = fs.realpathSync.native
|
|
130
|
+
? fs.realpathSync.native(targetPath)
|
|
131
|
+
: fs.realpathSync(targetPath);
|
|
132
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
133
|
+
} catch {
|
|
134
|
+
const resolved = path.resolve(targetPath);
|
|
135
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isImplicitHomeDirectory(targetPath) {
|
|
140
|
+
return Boolean(targetPath) && canonicalPath(targetPath) === canonicalPath(os.homedir());
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isUserProfileLikeDirectory(targetPath) {
|
|
144
|
+
if (!targetPath) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const resolved = path.resolve(targetPath);
|
|
149
|
+
const normalized = resolved.replaceAll("\\", "/");
|
|
150
|
+
if (/^[A-Za-z]:\/Users\/[^/]+$/i.test(normalized)) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
if (/^\/Users\/[^/]+$/.test(normalized)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
if (/^\/home\/[^/]+$/.test(normalized)) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
102
162
|
function choosePreferredCandidate(candidates, directory) {
|
|
103
163
|
return candidates.find((candidate) => {
|
|
104
164
|
const relativeToLeaf = path.relative(candidate, directory);
|
|
105
165
|
return relativeToLeaf
|
|
106
166
|
.split(/[\\/]+/)
|
|
107
167
|
.filter(Boolean)
|
|
108
|
-
.some((segment) =>
|
|
168
|
+
.some((segment) => PREFERRED_ANCESTOR_SEGMENTS.has(segment.toLowerCase()));
|
|
109
169
|
})
|
|
110
170
|
|| candidates.find((candidate) => !hasIgnoredProjectBasename(candidate))
|
|
111
171
|
|| candidates[0]
|
|
@@ -233,6 +293,10 @@ export function findPreferredRepoRootFromPath(startPath) {
|
|
|
233
293
|
return current;
|
|
234
294
|
}
|
|
235
295
|
|
|
296
|
+
if (isImplicitHomeDirectory(current) || isUserProfileLikeDirectory(current)) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
236
300
|
if (looksLikeStrongProjectRoot(current)) {
|
|
237
301
|
strongCandidates.push(current);
|
|
238
302
|
continue;
|
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);
|