helloloop 0.3.1 → 0.7.0
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 +4 -4
- package/README.md +194 -81
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +17 -13
- package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +12 -5
- package/hosts/gemini/extension/GEMINI.md +14 -7
- package/hosts/gemini/extension/commands/helloloop.toml +17 -12
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/skills/helloloop/SKILL.md +18 -7
- package/src/analyze_confirmation.mjs +29 -5
- package/src/analyze_prompt.mjs +5 -1
- package/src/analyze_user_input.mjs +20 -2
- package/src/analyzer.mjs +130 -43
- package/src/cli.mjs +32 -492
- package/src/cli_analyze_command.mjs +248 -0
- package/src/cli_args.mjs +106 -0
- package/src/cli_command_handlers.mjs +120 -0
- package/src/cli_context.mjs +31 -0
- package/src/cli_render.mjs +70 -0
- package/src/cli_support.mjs +11 -14
- package/src/completion_review.mjs +243 -0
- package/src/config.mjs +51 -0
- package/src/discovery.mjs +21 -2
- package/src/discovery_prompt.mjs +2 -27
- package/src/email_notification.mjs +343 -0
- package/src/engine_metadata.mjs +79 -0
- package/src/engine_process_support.mjs +294 -0
- package/src/engine_selection.mjs +335 -0
- package/src/engine_selection_failure.mjs +51 -0
- package/src/engine_selection_messages.mjs +119 -0
- package/src/engine_selection_probe.mjs +78 -0
- package/src/engine_selection_prompt.mjs +48 -0
- package/src/engine_selection_settings.mjs +104 -0
- package/src/global_config.mjs +21 -0
- package/src/guardrails.mjs +15 -4
- package/src/install.mjs +6 -405
- package/src/install_claude.mjs +189 -0
- package/src/install_codex.mjs +114 -0
- package/src/install_gemini.mjs +43 -0
- package/src/install_shared.mjs +138 -0
- package/src/process.mjs +567 -100
- package/src/prompt.mjs +9 -5
- package/src/prompt_session.mjs +40 -0
- package/src/runner.mjs +3 -341
- package/src/runner_execute_task.mjs +255 -0
- package/src/runner_execution_support.mjs +146 -0
- package/src/runner_loop.mjs +106 -0
- package/src/runner_once.mjs +29 -0
- package/src/runner_status.mjs +104 -0
- package/src/runtime_recovery.mjs +302 -0
- package/src/shell_invocation.mjs +16 -0
- package/templates/analysis-output.schema.json +0 -1
- package/templates/policy.template.json +25 -0
- package/templates/project.template.json +2 -0
- package/templates/task-review-output.schema.json +70 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { fileExists, readJson, writeJson } from "./common.mjs";
|
|
5
|
+
import { normalizeEngineName } from "./engine_metadata.mjs";
|
|
6
|
+
|
|
7
|
+
function defaultEmailNotificationSettings() {
|
|
8
|
+
return {
|
|
9
|
+
enabled: false,
|
|
10
|
+
to: [],
|
|
11
|
+
from: "",
|
|
12
|
+
smtp: {
|
|
13
|
+
host: "",
|
|
14
|
+
port: 465,
|
|
15
|
+
secure: true,
|
|
16
|
+
starttls: false,
|
|
17
|
+
username: "",
|
|
18
|
+
usernameEnv: "",
|
|
19
|
+
password: "",
|
|
20
|
+
passwordEnv: "",
|
|
21
|
+
timeoutSeconds: 30,
|
|
22
|
+
rejectUnauthorized: true,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function defaultUserSettings() {
|
|
28
|
+
return {
|
|
29
|
+
defaultEngine: "",
|
|
30
|
+
lastSelectedEngine: "",
|
|
31
|
+
notifications: {
|
|
32
|
+
email: defaultEmailNotificationSettings(),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveUserSettingsHome() {
|
|
38
|
+
return String(process.env.HELLOLOOP_HOME || "").trim()
|
|
39
|
+
|| path.join(os.homedir(), ".helloloop");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveUserSettingsFile(userSettingsFile = "") {
|
|
43
|
+
return userSettingsFile
|
|
44
|
+
|| String(process.env.HELLOLOOP_SETTINGS_FILE || "").trim()
|
|
45
|
+
|| path.join(resolveUserSettingsHome(), "settings.json");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeEmailNotificationSettings(emailSettings = {}) {
|
|
49
|
+
const defaults = defaultEmailNotificationSettings();
|
|
50
|
+
const smtp = emailSettings?.smtp || {};
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...defaults,
|
|
54
|
+
...emailSettings,
|
|
55
|
+
to: Array.isArray(emailSettings?.to)
|
|
56
|
+
? emailSettings.to.map((item) => String(item || "").trim()).filter(Boolean)
|
|
57
|
+
: (typeof emailSettings?.to === "string" && emailSettings.to.trim() ? [emailSettings.to.trim()] : []),
|
|
58
|
+
smtp: {
|
|
59
|
+
...defaults.smtp,
|
|
60
|
+
...smtp,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function loadUserSettingsDocument(options = {}) {
|
|
66
|
+
const settingsFile = resolveUserSettingsFile(options.userSettingsFile);
|
|
67
|
+
const settings = fileExists(settingsFile) ? readJson(settingsFile) : {};
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
...defaultUserSettings(),
|
|
71
|
+
...settings,
|
|
72
|
+
defaultEngine: normalizeEngineName(settings?.defaultEngine),
|
|
73
|
+
lastSelectedEngine: normalizeEngineName(settings?.lastSelectedEngine),
|
|
74
|
+
notifications: {
|
|
75
|
+
...(settings?.notifications || {}),
|
|
76
|
+
email: normalizeEmailNotificationSettings(settings?.notifications?.email || {}),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function loadUserSettings(options = {}) {
|
|
82
|
+
const settings = loadUserSettingsDocument(options);
|
|
83
|
+
return {
|
|
84
|
+
defaultEngine: settings.defaultEngine,
|
|
85
|
+
lastSelectedEngine: settings.lastSelectedEngine,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function saveUserSettings(settings, options = {}) {
|
|
90
|
+
const currentSettings = loadUserSettingsDocument(options);
|
|
91
|
+
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
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { fileExists } from "./common.mjs";
|
|
2
|
+
import { loadUserSettingsDocument, resolveUserSettingsFile } from "./engine_selection_settings.mjs";
|
|
3
|
+
|
|
4
|
+
export function resolveGlobalConfigFile(explicitFile = "") {
|
|
5
|
+
return resolveUserSettingsFile(explicitFile);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function loadGlobalConfig(options = {}) {
|
|
9
|
+
const configFile = resolveGlobalConfigFile(options.globalConfigFile);
|
|
10
|
+
const loaded = loadUserSettingsDocument({
|
|
11
|
+
userSettingsFile: configFile,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
...loaded,
|
|
16
|
+
_meta: {
|
|
17
|
+
configFile,
|
|
18
|
+
exists: fileExists(configFile),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
package/src/guardrails.mjs
CHANGED
|
@@ -35,12 +35,19 @@ const mandatoryGuardrails = [
|
|
|
35
35
|
"目标实现必须兼容 Windows、macOS、Linux,禁止硬编码平台专属路径分隔符或 shell 语法。",
|
|
36
36
|
];
|
|
37
37
|
|
|
38
|
+
const mandatoryEngineeringPrinciples = [
|
|
39
|
+
"代码是唯一事实源;文档与代码不一致时,以当前代码、测试和真实目录结构为准。",
|
|
40
|
+
"代码体积控制:文件/类超过 300 行、函数/方法超过 40 行时必须评估是否拆分;文件/类超过 400 行、函数/方法超过 60 行时,除生成代码、大型测试夹具、迁移脚本、协议常量表等例外外,必须在完成功能后按职责拆分。",
|
|
41
|
+
"禁止通过压缩代码排版、删除必要空行、合并本应独立的函数、缩短命名等方式规避行数;允许按职责拆模块、拆子组件、拆 hooks/services/adapters/mappers、拆类型定义与常量文件;有冗余时应精简死代码、重复逻辑和过时注释。",
|
|
42
|
+
"仅为复杂逻辑添加注释;新增公共函数必须写简洁 docstring。",
|
|
43
|
+
"不添加不必要的抽象层。",
|
|
44
|
+
"所有产出都必须达到专业级水准:编码任务要求架构清晰、代码健壮、UI 精致、交互流畅;非编码任务要求逻辑严密、结构清晰、表达专业、格式规范,不接受“能用就行”的交付。",
|
|
45
|
+
];
|
|
46
|
+
|
|
38
47
|
const defaultProjectConstraints = [
|
|
39
|
-
"
|
|
40
|
-
"
|
|
48
|
+
"用户需求明确且当前任务可直接完成时,必须一次性完成本轮应交付的全部工作;禁止做一半后用“如果你要”“是否继续”等话术中途停下,只有真实歧义、关键信息缺失或必须用户决策时才允许确认。",
|
|
49
|
+
"最终回复禁止添加“如果你要我可以继续”“如果你需要进一步…”“希望这对你有帮助”等客套收尾;完成就直接结束,只保留必要的结果、验证和剩余风险。",
|
|
41
50
|
"所有文件修改必须使用当前环境提供的安全编辑方式,例如 apply_patch,而不是危险的原地批量命令。",
|
|
42
|
-
"不得通过压缩代码、删除必要空行、缩短命名来规避体积控制;单文件超过 400 行后按职责拆分。",
|
|
43
|
-
"不添加不必要的抽象层;仅为复杂逻辑添加必要注释,新增公共函数补简洁说明。",
|
|
44
51
|
"完成前必须主动运行验证;失败后先分析根因,再修复并重跑。",
|
|
45
52
|
"不允许静默失败、静默降级、静默回退;遇到阻塞必须明确说明原因。",
|
|
46
53
|
];
|
|
@@ -49,6 +56,10 @@ export function listMandatoryGuardrails() {
|
|
|
49
56
|
return [...mandatoryGuardrails];
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
export function listMandatoryEngineeringPrinciples() {
|
|
60
|
+
return [...mandatoryEngineeringPrinciples];
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
export function hasCustomProjectConstraints(items = []) {
|
|
53
64
|
return uniqueRules(items).length > 0;
|
|
54
65
|
}
|
package/src/install.mjs
CHANGED
|
@@ -1,415 +1,14 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
1
|
import path from "node:path";
|
|
4
2
|
import { fileURLToPath } from "node:url";
|
|
5
3
|
|
|
6
|
-
import {
|
|
4
|
+
import { installClaudeHost, uninstallClaudeHost } from "./install_claude.mjs";
|
|
5
|
+
import { installCodexHost, uninstallCodexHost } from "./install_codex.mjs";
|
|
6
|
+
import { installGeminiHost, uninstallGeminiHost } from "./install_gemini.mjs";
|
|
7
|
+
import { runtimeBundleEntries, supportedHosts } from "./install_shared.mjs";
|
|
7
8
|
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
10
|
const __dirname = path.dirname(__filename);
|
|
10
11
|
|
|
11
|
-
export const runtimeBundleEntries = [
|
|
12
|
-
".claude-plugin",
|
|
13
|
-
".codex-plugin",
|
|
14
|
-
"LICENSE",
|
|
15
|
-
"README.md",
|
|
16
|
-
"bin",
|
|
17
|
-
"hosts",
|
|
18
|
-
"package.json",
|
|
19
|
-
"scripts",
|
|
20
|
-
"skills",
|
|
21
|
-
"src",
|
|
22
|
-
"templates",
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
const codexBundleEntries = runtimeBundleEntries.filter((entry) => ![
|
|
26
|
-
".claude-plugin",
|
|
27
|
-
"hosts",
|
|
28
|
-
].includes(entry));
|
|
29
|
-
|
|
30
|
-
const supportedHosts = ["codex", "claude", "gemini"];
|
|
31
|
-
const CLAUDE_MARKETPLACE_NAME = "helloloop-local";
|
|
32
|
-
const CLAUDE_PLUGIN_KEY = "helloloop@helloloop-local";
|
|
33
|
-
|
|
34
|
-
function resolveHomeDir(homeDir, defaultDirName) {
|
|
35
|
-
return path.resolve(homeDir || path.join(os.homedir(), defaultDirName));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function assertPathInside(parentDir, targetDir, label) {
|
|
39
|
-
const relative = path.relative(parentDir, targetDir);
|
|
40
|
-
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
41
|
-
throw new Error(`${label} 超出允许范围:${targetDir}`);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function removeTargetIfNeeded(targetPath, force) {
|
|
46
|
-
if (!fileExists(targetPath)) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
if (!force) {
|
|
50
|
-
throw new Error(`目标目录已存在:${targetPath}。若要覆盖,请追加 --force。`);
|
|
51
|
-
}
|
|
52
|
-
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function removePathIfExists(targetPath) {
|
|
56
|
-
if (!fileExists(targetPath)) {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function copyBundleEntries(bundleRoot, targetRoot, entries) {
|
|
64
|
-
for (const entry of entries) {
|
|
65
|
-
const sourcePath = path.join(bundleRoot, entry);
|
|
66
|
-
if (!fileExists(sourcePath)) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
fs.cpSync(sourcePath, path.join(targetRoot, entry), {
|
|
71
|
-
force: true,
|
|
72
|
-
recursive: true,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function copyDirectory(sourceRoot, targetRoot) {
|
|
78
|
-
fs.cpSync(sourceRoot, targetRoot, {
|
|
79
|
-
force: true,
|
|
80
|
-
recursive: true,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function updateCodexMarketplace(marketplaceFile) {
|
|
85
|
-
const marketplace = fileExists(marketplaceFile)
|
|
86
|
-
? readJson(marketplaceFile)
|
|
87
|
-
: {
|
|
88
|
-
name: "local-plugins",
|
|
89
|
-
interface: {
|
|
90
|
-
displayName: "Local Plugins",
|
|
91
|
-
},
|
|
92
|
-
plugins: [],
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
marketplace.interface = marketplace.interface || {
|
|
96
|
-
displayName: "Local Plugins",
|
|
97
|
-
};
|
|
98
|
-
marketplace.plugins = Array.isArray(marketplace.plugins) ? marketplace.plugins : [];
|
|
99
|
-
|
|
100
|
-
const nextEntry = {
|
|
101
|
-
name: "helloloop",
|
|
102
|
-
source: {
|
|
103
|
-
source: "local",
|
|
104
|
-
path: "./plugins/helloloop",
|
|
105
|
-
},
|
|
106
|
-
policy: {
|
|
107
|
-
installation: "AVAILABLE",
|
|
108
|
-
authentication: "ON_INSTALL",
|
|
109
|
-
},
|
|
110
|
-
category: "Coding",
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const existingIndex = marketplace.plugins.findIndex((plugin) => plugin?.name === "helloloop");
|
|
114
|
-
if (existingIndex >= 0) {
|
|
115
|
-
marketplace.plugins.splice(existingIndex, 1, nextEntry);
|
|
116
|
-
} else {
|
|
117
|
-
marketplace.plugins.push(nextEntry);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
writeJson(marketplaceFile, marketplace);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function removeCodexMarketplaceEntry(marketplaceFile) {
|
|
124
|
-
if (!fileExists(marketplaceFile)) {
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const marketplace = readJson(marketplaceFile);
|
|
129
|
-
const plugins = Array.isArray(marketplace.plugins) ? marketplace.plugins : [];
|
|
130
|
-
const nextPlugins = plugins.filter((plugin) => plugin?.name !== "helloloop");
|
|
131
|
-
if (nextPlugins.length === plugins.length) {
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
marketplace.plugins = nextPlugins;
|
|
136
|
-
writeJson(marketplaceFile, marketplace);
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function updateClaudeSettings(settingsFile, marketplaceRoot) {
|
|
141
|
-
const settings = fileExists(settingsFile)
|
|
142
|
-
? readJson(settingsFile)
|
|
143
|
-
: {};
|
|
144
|
-
|
|
145
|
-
settings.extraKnownMarketplaces = settings.extraKnownMarketplaces || {};
|
|
146
|
-
settings.enabledPlugins = settings.enabledPlugins || {};
|
|
147
|
-
|
|
148
|
-
settings.extraKnownMarketplaces[CLAUDE_MARKETPLACE_NAME] = {
|
|
149
|
-
source: {
|
|
150
|
-
source: "directory",
|
|
151
|
-
path: marketplaceRoot,
|
|
152
|
-
},
|
|
153
|
-
};
|
|
154
|
-
settings.enabledPlugins[CLAUDE_PLUGIN_KEY] = true;
|
|
155
|
-
|
|
156
|
-
writeJson(settingsFile, settings);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function removeClaudeSettingsEntries(settingsFile) {
|
|
160
|
-
if (!fileExists(settingsFile)) {
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const settings = readJson(settingsFile);
|
|
165
|
-
let changed = false;
|
|
166
|
-
|
|
167
|
-
if (settings.extraKnownMarketplaces && Object.hasOwn(settings.extraKnownMarketplaces, CLAUDE_MARKETPLACE_NAME)) {
|
|
168
|
-
delete settings.extraKnownMarketplaces[CLAUDE_MARKETPLACE_NAME];
|
|
169
|
-
changed = true;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (settings.enabledPlugins && Object.hasOwn(settings.enabledPlugins, CLAUDE_PLUGIN_KEY)) {
|
|
173
|
-
delete settings.enabledPlugins[CLAUDE_PLUGIN_KEY];
|
|
174
|
-
changed = true;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (changed) {
|
|
178
|
-
writeJson(settingsFile, settings);
|
|
179
|
-
}
|
|
180
|
-
return changed;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function updateClaudeKnownMarketplaces(knownMarketplacesFile, marketplaceRoot, updatedAt) {
|
|
184
|
-
const knownMarketplaces = fileExists(knownMarketplacesFile)
|
|
185
|
-
? readJson(knownMarketplacesFile)
|
|
186
|
-
: {};
|
|
187
|
-
|
|
188
|
-
knownMarketplaces[CLAUDE_MARKETPLACE_NAME] = {
|
|
189
|
-
source: {
|
|
190
|
-
source: "directory",
|
|
191
|
-
path: marketplaceRoot,
|
|
192
|
-
},
|
|
193
|
-
installLocation: marketplaceRoot,
|
|
194
|
-
lastUpdated: updatedAt,
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
writeJson(knownMarketplacesFile, knownMarketplaces);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function removeClaudeKnownMarketplace(knownMarketplacesFile) {
|
|
201
|
-
if (!fileExists(knownMarketplacesFile)) {
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const knownMarketplaces = readJson(knownMarketplacesFile);
|
|
206
|
-
if (!Object.hasOwn(knownMarketplaces, CLAUDE_MARKETPLACE_NAME)) {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
delete knownMarketplaces[CLAUDE_MARKETPLACE_NAME];
|
|
211
|
-
writeJson(knownMarketplacesFile, knownMarketplaces);
|
|
212
|
-
return true;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function updateClaudeInstalledPlugins(installedPluginsFile, pluginRoot, pluginVersion, updatedAt) {
|
|
216
|
-
const installedPlugins = fileExists(installedPluginsFile)
|
|
217
|
-
? readJson(installedPluginsFile)
|
|
218
|
-
: {
|
|
219
|
-
version: 2,
|
|
220
|
-
plugins: {},
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
installedPlugins.version = 2;
|
|
224
|
-
installedPlugins.plugins = installedPlugins.plugins || {};
|
|
225
|
-
installedPlugins.plugins[CLAUDE_PLUGIN_KEY] = [
|
|
226
|
-
{
|
|
227
|
-
scope: "user",
|
|
228
|
-
installPath: pluginRoot,
|
|
229
|
-
version: pluginVersion,
|
|
230
|
-
installedAt: updatedAt,
|
|
231
|
-
lastUpdated: updatedAt,
|
|
232
|
-
},
|
|
233
|
-
];
|
|
234
|
-
|
|
235
|
-
writeJson(installedPluginsFile, installedPlugins);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function removeClaudeInstalledPlugin(installedPluginsFile) {
|
|
239
|
-
if (!fileExists(installedPluginsFile)) {
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const installedPlugins = readJson(installedPluginsFile);
|
|
244
|
-
if (!installedPlugins.plugins || !Object.hasOwn(installedPlugins.plugins, CLAUDE_PLUGIN_KEY)) {
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
delete installedPlugins.plugins[CLAUDE_PLUGIN_KEY];
|
|
249
|
-
writeJson(installedPluginsFile, installedPlugins);
|
|
250
|
-
return true;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function installCodexHost(bundleRoot, options) {
|
|
254
|
-
const resolvedCodexHome = resolveHomeDir(options.codexHome, ".codex");
|
|
255
|
-
const targetPluginsRoot = path.join(resolvedCodexHome, "plugins");
|
|
256
|
-
const targetPluginRoot = path.join(targetPluginsRoot, "helloloop");
|
|
257
|
-
const marketplaceFile = path.join(resolvedCodexHome, ".agents", "plugins", "marketplace.json");
|
|
258
|
-
const manifestFile = path.join(bundleRoot, ".codex-plugin", "plugin.json");
|
|
259
|
-
|
|
260
|
-
if (!fileExists(manifestFile)) {
|
|
261
|
-
throw new Error(`未找到 Codex 插件 manifest:${manifestFile}`);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
assertPathInside(resolvedCodexHome, targetPluginRoot, "Codex 目标插件目录");
|
|
265
|
-
removeTargetIfNeeded(targetPluginRoot, options.force);
|
|
266
|
-
|
|
267
|
-
ensureDir(targetPluginsRoot);
|
|
268
|
-
ensureDir(targetPluginRoot);
|
|
269
|
-
copyBundleEntries(bundleRoot, targetPluginRoot, codexBundleEntries);
|
|
270
|
-
|
|
271
|
-
const gitMetadataPath = path.join(targetPluginRoot, ".git");
|
|
272
|
-
if (fileExists(gitMetadataPath)) {
|
|
273
|
-
fs.rmSync(gitMetadataPath, { recursive: true, force: true });
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
ensureDir(path.dirname(marketplaceFile));
|
|
277
|
-
updateCodexMarketplace(marketplaceFile);
|
|
278
|
-
|
|
279
|
-
return {
|
|
280
|
-
host: "codex",
|
|
281
|
-
displayName: "Codex",
|
|
282
|
-
targetRoot: targetPluginRoot,
|
|
283
|
-
marketplaceFile,
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function uninstallCodexHost(options) {
|
|
288
|
-
const resolvedCodexHome = resolveHomeDir(options.codexHome, ".codex");
|
|
289
|
-
const targetPluginRoot = path.join(resolvedCodexHome, "plugins", "helloloop");
|
|
290
|
-
const marketplaceFile = path.join(resolvedCodexHome, ".agents", "plugins", "marketplace.json");
|
|
291
|
-
|
|
292
|
-
const removedPlugin = removePathIfExists(targetPluginRoot);
|
|
293
|
-
const removedMarketplace = removeCodexMarketplaceEntry(marketplaceFile);
|
|
294
|
-
|
|
295
|
-
return {
|
|
296
|
-
host: "codex",
|
|
297
|
-
displayName: "Codex",
|
|
298
|
-
targetRoot: targetPluginRoot,
|
|
299
|
-
removed: removedPlugin || removedMarketplace,
|
|
300
|
-
marketplaceFile,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function installClaudeHost(bundleRoot, options) {
|
|
305
|
-
const resolvedClaudeHome = resolveHomeDir(options.claudeHome, ".claude");
|
|
306
|
-
const sourceMarketplaceRoot = path.join(bundleRoot, "hosts", "claude", "marketplace");
|
|
307
|
-
const sourceManifest = path.join(bundleRoot, ".claude-plugin", "plugin.json");
|
|
308
|
-
const pluginVersion = readJson(sourceManifest).version || readJson(path.join(bundleRoot, "package.json")).version;
|
|
309
|
-
const targetPluginsRoot = path.join(resolvedClaudeHome, "plugins");
|
|
310
|
-
const targetMarketplaceRoot = path.join(targetPluginsRoot, "marketplaces", CLAUDE_MARKETPLACE_NAME);
|
|
311
|
-
const targetCachePluginsRoot = path.join(targetPluginsRoot, "cache", CLAUDE_MARKETPLACE_NAME, "helloloop");
|
|
312
|
-
const targetInstalledPluginRoot = path.join(targetCachePluginsRoot, pluginVersion);
|
|
313
|
-
const knownMarketplacesFile = path.join(targetPluginsRoot, "known_marketplaces.json");
|
|
314
|
-
const installedPluginsFile = path.join(targetPluginsRoot, "installed_plugins.json");
|
|
315
|
-
const settingsFile = path.join(resolvedClaudeHome, "settings.json");
|
|
316
|
-
|
|
317
|
-
if (!fileExists(sourceManifest)) {
|
|
318
|
-
throw new Error(`未找到 Claude 插件 manifest:${sourceManifest}`);
|
|
319
|
-
}
|
|
320
|
-
if (!fileExists(path.join(sourceMarketplaceRoot, ".claude-plugin", "marketplace.json"))) {
|
|
321
|
-
throw new Error(`未找到 Claude marketplace 模板:${sourceMarketplaceRoot}`);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
assertPathInside(resolvedClaudeHome, targetMarketplaceRoot, "Claude marketplace 目录");
|
|
325
|
-
assertPathInside(resolvedClaudeHome, targetInstalledPluginRoot, "Claude 插件缓存目录");
|
|
326
|
-
removeTargetIfNeeded(targetMarketplaceRoot, options.force);
|
|
327
|
-
removeTargetIfNeeded(targetCachePluginsRoot, options.force);
|
|
328
|
-
|
|
329
|
-
ensureDir(path.dirname(targetMarketplaceRoot));
|
|
330
|
-
ensureDir(path.dirname(targetInstalledPluginRoot));
|
|
331
|
-
copyDirectory(sourceMarketplaceRoot, targetMarketplaceRoot);
|
|
332
|
-
copyDirectory(path.join(sourceMarketplaceRoot, "plugins", "helloloop"), targetInstalledPluginRoot);
|
|
333
|
-
ensureDir(targetPluginsRoot);
|
|
334
|
-
const updatedAt = nowIso();
|
|
335
|
-
updateClaudeSettings(settingsFile, targetMarketplaceRoot);
|
|
336
|
-
updateClaudeKnownMarketplaces(knownMarketplacesFile, targetMarketplaceRoot, updatedAt);
|
|
337
|
-
updateClaudeInstalledPlugins(installedPluginsFile, targetInstalledPluginRoot, pluginVersion, updatedAt);
|
|
338
|
-
|
|
339
|
-
return {
|
|
340
|
-
host: "claude",
|
|
341
|
-
displayName: "Claude",
|
|
342
|
-
targetRoot: targetInstalledPluginRoot,
|
|
343
|
-
marketplaceFile: path.join(targetMarketplaceRoot, ".claude-plugin", "marketplace.json"),
|
|
344
|
-
settingsFile,
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function uninstallClaudeHost(options) {
|
|
349
|
-
const resolvedClaudeHome = resolveHomeDir(options.claudeHome, ".claude");
|
|
350
|
-
const targetPluginsRoot = path.join(resolvedClaudeHome, "plugins");
|
|
351
|
-
const targetMarketplaceRoot = path.join(targetPluginsRoot, "marketplaces", CLAUDE_MARKETPLACE_NAME);
|
|
352
|
-
const targetCachePluginsRoot = path.join(targetPluginsRoot, "cache", CLAUDE_MARKETPLACE_NAME);
|
|
353
|
-
const knownMarketplacesFile = path.join(targetPluginsRoot, "known_marketplaces.json");
|
|
354
|
-
const installedPluginsFile = path.join(targetPluginsRoot, "installed_plugins.json");
|
|
355
|
-
const settingsFile = path.join(resolvedClaudeHome, "settings.json");
|
|
356
|
-
|
|
357
|
-
const removedMarketplaceDir = removePathIfExists(targetMarketplaceRoot);
|
|
358
|
-
const removedCacheDir = removePathIfExists(targetCachePluginsRoot);
|
|
359
|
-
const removedKnownMarketplace = removeClaudeKnownMarketplace(knownMarketplacesFile);
|
|
360
|
-
const removedInstalledPlugin = removeClaudeInstalledPlugin(installedPluginsFile);
|
|
361
|
-
const removedSettingsEntries = removeClaudeSettingsEntries(settingsFile);
|
|
362
|
-
|
|
363
|
-
return {
|
|
364
|
-
host: "claude",
|
|
365
|
-
displayName: "Claude",
|
|
366
|
-
targetRoot: targetCachePluginsRoot,
|
|
367
|
-
removed: [
|
|
368
|
-
removedMarketplaceDir,
|
|
369
|
-
removedCacheDir,
|
|
370
|
-
removedKnownMarketplace,
|
|
371
|
-
removedInstalledPlugin,
|
|
372
|
-
removedSettingsEntries,
|
|
373
|
-
].some(Boolean),
|
|
374
|
-
marketplaceFile: path.join(targetMarketplaceRoot, ".claude-plugin", "marketplace.json"),
|
|
375
|
-
settingsFile,
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function installGeminiHost(bundleRoot, options) {
|
|
380
|
-
const resolvedGeminiHome = resolveHomeDir(options.geminiHome, ".gemini");
|
|
381
|
-
const sourceExtensionRoot = path.join(bundleRoot, "hosts", "gemini", "extension");
|
|
382
|
-
const targetExtensionRoot = path.join(resolvedGeminiHome, "extensions", "helloloop");
|
|
383
|
-
|
|
384
|
-
if (!fileExists(path.join(sourceExtensionRoot, "gemini-extension.json"))) {
|
|
385
|
-
throw new Error(`未找到 Gemini 扩展清单:${sourceExtensionRoot}`);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
assertPathInside(resolvedGeminiHome, targetExtensionRoot, "Gemini 扩展目录");
|
|
389
|
-
removeTargetIfNeeded(targetExtensionRoot, options.force);
|
|
390
|
-
|
|
391
|
-
ensureDir(path.dirname(targetExtensionRoot));
|
|
392
|
-
copyDirectory(sourceExtensionRoot, targetExtensionRoot);
|
|
393
|
-
|
|
394
|
-
return {
|
|
395
|
-
host: "gemini",
|
|
396
|
-
displayName: "Gemini",
|
|
397
|
-
targetRoot: targetExtensionRoot,
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function uninstallGeminiHost(options) {
|
|
402
|
-
const resolvedGeminiHome = resolveHomeDir(options.geminiHome, ".gemini");
|
|
403
|
-
const targetExtensionRoot = path.join(resolvedGeminiHome, "extensions", "helloloop");
|
|
404
|
-
|
|
405
|
-
return {
|
|
406
|
-
host: "gemini",
|
|
407
|
-
displayName: "Gemini",
|
|
408
|
-
targetRoot: targetExtensionRoot,
|
|
409
|
-
removed: removePathIfExists(targetExtensionRoot),
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
|
|
413
12
|
function resolveInstallHosts(hostOption) {
|
|
414
13
|
const normalized = String(hostOption || "codex").trim().toLowerCase();
|
|
415
14
|
if (normalized === "all") {
|
|
@@ -421,6 +20,8 @@ function resolveInstallHosts(hostOption) {
|
|
|
421
20
|
return [normalized];
|
|
422
21
|
}
|
|
423
22
|
|
|
23
|
+
export { runtimeBundleEntries };
|
|
24
|
+
|
|
424
25
|
export function installPluginBundle(options = {}) {
|
|
425
26
|
const bundleRoot = path.resolve(options.bundleRoot || path.join(__dirname, ".."));
|
|
426
27
|
const selectedHosts = resolveInstallHosts(options.host);
|