helloloop 0.1.2 → 0.2.1
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 +14 -0
- package/.codex-plugin/plugin.json +4 -4
- package/README.md +310 -124
- package/hosts/claude/marketplace/.claude-plugin/marketplace.json +16 -0
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +9 -0
- package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +28 -0
- package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +38 -0
- package/hosts/gemini/extension/GEMINI.md +29 -0
- package/hosts/gemini/extension/commands/helloloop.toml +24 -0
- package/hosts/gemini/extension/gemini-extension.json +13 -0
- package/package.json +5 -3
- package/skills/helloloop/SKILL.md +34 -10
- package/src/analyze_confirmation.mjs +128 -0
- package/src/analyzer.mjs +2 -2
- package/src/cli.mjs +79 -86
- package/src/cli_support.mjs +327 -0
- package/src/install.mjs +200 -22
- package/src/shell_invocation.mjs +21 -7
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
|
|
5
|
+
import { analyzeExecution, summarizeBacklog } from "./backlog.mjs";
|
|
6
|
+
import { fileExists, readJson } from "./common.mjs";
|
|
7
|
+
import { resolveCliInvocation, resolveCodexInvocation } from "./shell_invocation.mjs";
|
|
8
|
+
|
|
9
|
+
function probeCodexVersion() {
|
|
10
|
+
const invocation = resolveCodexInvocation();
|
|
11
|
+
if (invocation.error) {
|
|
12
|
+
return {
|
|
13
|
+
ok: false,
|
|
14
|
+
detail: invocation.error,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const codexVersion = spawnSync(invocation.command, [...invocation.argsPrefix, "--version"], {
|
|
19
|
+
encoding: "utf8",
|
|
20
|
+
shell: invocation.shell,
|
|
21
|
+
});
|
|
22
|
+
const ok = codexVersion.status === 0;
|
|
23
|
+
return {
|
|
24
|
+
ok,
|
|
25
|
+
detail: ok
|
|
26
|
+
? String(codexVersion.stdout || "").trim()
|
|
27
|
+
: String(codexVersion.stderr || codexVersion.error || "无法执行 codex --version").trim(),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function collectDoctorChecks(context) {
|
|
32
|
+
const codexVersion = probeCodexVersion();
|
|
33
|
+
return [
|
|
34
|
+
{
|
|
35
|
+
name: "codex CLI",
|
|
36
|
+
ok: codexVersion.ok,
|
|
37
|
+
detail: codexVersion.detail,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "backlog.json",
|
|
41
|
+
ok: fileExists(context.backlogFile),
|
|
42
|
+
detail: context.backlogFile,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "policy.json",
|
|
46
|
+
ok: fileExists(context.policyFile),
|
|
47
|
+
detail: context.policyFile,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "verify.yaml",
|
|
51
|
+
ok: fileExists(context.repoVerifyFile),
|
|
52
|
+
detail: context.repoVerifyFile,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "project.json",
|
|
56
|
+
ok: fileExists(context.projectFile),
|
|
57
|
+
detail: context.projectFile,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "plugin manifest",
|
|
61
|
+
ok: fileExists(context.pluginManifestFile),
|
|
62
|
+
detail: context.pluginManifestFile,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "plugin skill",
|
|
66
|
+
ok: fileExists(context.skillFile),
|
|
67
|
+
detail: context.skillFile,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "install script",
|
|
71
|
+
ok: fileExists(context.installScriptFile),
|
|
72
|
+
detail: context.installScriptFile,
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function probeNamedCliVersion(commandName, toolDisplayName) {
|
|
78
|
+
const invocation = resolveCliInvocation({
|
|
79
|
+
commandName,
|
|
80
|
+
toolDisplayName,
|
|
81
|
+
});
|
|
82
|
+
if (invocation.error) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
detail: invocation.error,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const result = spawnSync(invocation.command, [...invocation.argsPrefix, "--version"], {
|
|
90
|
+
encoding: "utf8",
|
|
91
|
+
shell: invocation.shell,
|
|
92
|
+
});
|
|
93
|
+
const ok = result.status === 0;
|
|
94
|
+
return {
|
|
95
|
+
ok,
|
|
96
|
+
detail: ok
|
|
97
|
+
? String(result.stdout || "").trim()
|
|
98
|
+
: String(result.stderr || result.error || `无法执行 ${commandName} --version`).trim(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function normalizeDoctorHosts(hostOption) {
|
|
103
|
+
const normalized = String(hostOption || "codex").trim().toLowerCase();
|
|
104
|
+
if (normalized === "all") {
|
|
105
|
+
return ["codex", "claude", "gemini"];
|
|
106
|
+
}
|
|
107
|
+
return [normalized];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function collectCodexDoctorChecks(context, options = {}) {
|
|
111
|
+
const checks = collectDoctorChecks(context);
|
|
112
|
+
if (options.codexHome) {
|
|
113
|
+
checks.push({
|
|
114
|
+
name: "codex installed plugin",
|
|
115
|
+
ok: fileExists(path.join(options.codexHome, "plugins", "helloloop", ".codex-plugin", "plugin.json")),
|
|
116
|
+
detail: path.join(options.codexHome, "plugins", "helloloop", ".codex-plugin", "plugin.json"),
|
|
117
|
+
});
|
|
118
|
+
checks.push({
|
|
119
|
+
name: "codex marketplace",
|
|
120
|
+
ok: fileExists(path.join(options.codexHome, ".agents", "plugins", "marketplace.json")),
|
|
121
|
+
detail: path.join(options.codexHome, ".agents", "plugins", "marketplace.json"),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return checks;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function collectClaudeDoctorChecks(context, options = {}) {
|
|
128
|
+
const claudeVersion = probeNamedCliVersion("claude", "Claude");
|
|
129
|
+
const checks = [
|
|
130
|
+
{
|
|
131
|
+
name: "claude CLI",
|
|
132
|
+
ok: claudeVersion.ok,
|
|
133
|
+
detail: claudeVersion.detail,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "claude plugin manifest",
|
|
137
|
+
ok: fileExists(path.join(context.bundleRoot, ".claude-plugin", "plugin.json")),
|
|
138
|
+
detail: path.join(context.bundleRoot, ".claude-plugin", "plugin.json"),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "claude marketplace manifest",
|
|
142
|
+
ok: fileExists(path.join(context.bundleRoot, "hosts", "claude", "marketplace", ".claude-plugin", "marketplace.json")),
|
|
143
|
+
detail: path.join(context.bundleRoot, "hosts", "claude", "marketplace", ".claude-plugin", "marketplace.json"),
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "claude command",
|
|
147
|
+
ok: fileExists(path.join(context.bundleRoot, "hosts", "claude", "marketplace", "plugins", "helloloop", "commands", "helloloop.md")),
|
|
148
|
+
detail: path.join(context.bundleRoot, "hosts", "claude", "marketplace", "plugins", "helloloop", "commands", "helloloop.md"),
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
if (options.claudeHome) {
|
|
153
|
+
const settingsFile = path.join(options.claudeHome, "settings.json");
|
|
154
|
+
const knownMarketplacesFile = path.join(options.claudeHome, "plugins", "known_marketplaces.json");
|
|
155
|
+
const installedPluginsFile = path.join(options.claudeHome, "plugins", "installed_plugins.json");
|
|
156
|
+
const settings = fileExists(settingsFile) ? readJson(settingsFile) : {};
|
|
157
|
+
const installedPlugins = fileExists(installedPluginsFile) ? readJson(installedPluginsFile) : {};
|
|
158
|
+
const installs = Array.isArray(installedPlugins?.plugins?.["helloloop@helloloop-local"])
|
|
159
|
+
? installedPlugins.plugins["helloloop@helloloop-local"]
|
|
160
|
+
: [];
|
|
161
|
+
const installedPluginRoot = installs[0]?.installPath
|
|
162
|
+
? String(installs[0].installPath)
|
|
163
|
+
: path.join(options.claudeHome, "plugins", "cache", "helloloop-local", "helloloop");
|
|
164
|
+
|
|
165
|
+
checks.push({
|
|
166
|
+
name: "claude installed marketplace",
|
|
167
|
+
ok: fileExists(path.join(options.claudeHome, "plugins", "marketplaces", "helloloop-local", ".claude-plugin", "marketplace.json")),
|
|
168
|
+
detail: path.join(options.claudeHome, "plugins", "marketplaces", "helloloop-local", ".claude-plugin", "marketplace.json"),
|
|
169
|
+
});
|
|
170
|
+
checks.push({
|
|
171
|
+
name: "claude marketplace registry",
|
|
172
|
+
ok: fileExists(knownMarketplacesFile),
|
|
173
|
+
detail: knownMarketplacesFile,
|
|
174
|
+
});
|
|
175
|
+
checks.push({
|
|
176
|
+
name: "claude installed plugin index",
|
|
177
|
+
ok: fileExists(installedPluginsFile),
|
|
178
|
+
detail: installedPluginsFile,
|
|
179
|
+
});
|
|
180
|
+
checks.push({
|
|
181
|
+
name: "claude installed plugin",
|
|
182
|
+
ok: fileExists(path.join(installedPluginRoot, ".claude-plugin", "plugin.json")),
|
|
183
|
+
detail: path.join(installedPluginRoot, ".claude-plugin", "plugin.json"),
|
|
184
|
+
});
|
|
185
|
+
checks.push({
|
|
186
|
+
name: "claude settings enabled",
|
|
187
|
+
ok: settings?.enabledPlugins?.["helloloop@helloloop-local"] === true,
|
|
188
|
+
detail: settingsFile,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return checks;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function collectGeminiDoctorChecks(context, options = {}) {
|
|
196
|
+
const geminiVersion = probeNamedCliVersion("gemini", "Gemini");
|
|
197
|
+
const checks = [
|
|
198
|
+
{
|
|
199
|
+
name: "gemini CLI",
|
|
200
|
+
ok: geminiVersion.ok,
|
|
201
|
+
detail: geminiVersion.detail,
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "gemini extension manifest",
|
|
205
|
+
ok: fileExists(path.join(context.bundleRoot, "hosts", "gemini", "extension", "gemini-extension.json")),
|
|
206
|
+
detail: path.join(context.bundleRoot, "hosts", "gemini", "extension", "gemini-extension.json"),
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: "gemini command",
|
|
210
|
+
ok: fileExists(path.join(context.bundleRoot, "hosts", "gemini", "extension", "commands", "helloloop.toml")),
|
|
211
|
+
detail: path.join(context.bundleRoot, "hosts", "gemini", "extension", "commands", "helloloop.toml"),
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: "gemini context file",
|
|
215
|
+
ok: fileExists(path.join(context.bundleRoot, "hosts", "gemini", "extension", "GEMINI.md")),
|
|
216
|
+
detail: path.join(context.bundleRoot, "hosts", "gemini", "extension", "GEMINI.md"),
|
|
217
|
+
},
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
if (options.geminiHome) {
|
|
221
|
+
checks.push({
|
|
222
|
+
name: "gemini installed extension",
|
|
223
|
+
ok: fileExists(path.join(options.geminiHome, "extensions", "helloloop", "gemini-extension.json")),
|
|
224
|
+
detail: path.join(options.geminiHome, "extensions", "helloloop", "gemini-extension.json"),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return checks;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function runDoctor(context, options = {}) {
|
|
232
|
+
const hosts = normalizeDoctorHosts(options.host);
|
|
233
|
+
const checks = hosts.flatMap((host) => {
|
|
234
|
+
if (host === "codex") return collectCodexDoctorChecks(context, options);
|
|
235
|
+
if (host === "claude") return collectClaudeDoctorChecks(context, options);
|
|
236
|
+
if (host === "gemini") return collectGeminiDoctorChecks(context, options);
|
|
237
|
+
return [];
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
for (const item of checks) {
|
|
241
|
+
console.log(`${item.ok ? "OK" : "FAIL"} ${item.name} ${item.detail}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (checks.every((item) => item.ok)) {
|
|
245
|
+
console.log("\nDoctor 结论:当前 HelloLoop 所选宿主与目标仓库已具备基本运行条件。");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (checks.some((item) => !item.ok)) {
|
|
249
|
+
process.exitCode = 1;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isAffirmativeAnswer(answer) {
|
|
254
|
+
const raw = String(answer || "").trim();
|
|
255
|
+
const normalized = raw.toLowerCase();
|
|
256
|
+
return [
|
|
257
|
+
"y",
|
|
258
|
+
"yes",
|
|
259
|
+
"ok",
|
|
260
|
+
"确认",
|
|
261
|
+
"是",
|
|
262
|
+
"继续",
|
|
263
|
+
"好的",
|
|
264
|
+
].includes(normalized) || ["确认", "是", "继续", "好的"].includes(raw);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function confirmAutoExecution() {
|
|
268
|
+
const readline = createInterface({
|
|
269
|
+
input: process.stdin,
|
|
270
|
+
output: process.stdout,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const answer = await readline.question("是否开始自动接续执行?输入 y / yes / 确认 继续,其它任意输入取消:");
|
|
275
|
+
return isAffirmativeAnswer(answer);
|
|
276
|
+
} finally {
|
|
277
|
+
readline.close();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function renderAnalyzeStopMessage(reason) {
|
|
282
|
+
return reason || "当前没有可自动执行的任务。";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function renderAutoRunSummary(context, backlog, results, options = {}) {
|
|
286
|
+
const summary = summarizeBacklog(backlog);
|
|
287
|
+
const execution = analyzeExecution(backlog, options);
|
|
288
|
+
const lines = [
|
|
289
|
+
"自动执行结果",
|
|
290
|
+
"============",
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
if (!results.length) {
|
|
294
|
+
lines.push("- 本轮未执行任何任务");
|
|
295
|
+
} else {
|
|
296
|
+
for (const item of results) {
|
|
297
|
+
if (!item.task) {
|
|
298
|
+
lines.push(`- 已停止:${item.summary || item.kind || "没有更多任务"}`);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
lines.push(`- ${item.ok ? "完成任务" : "失败任务"}:${item.task.title}`);
|
|
302
|
+
if (!item.ok && item.summary) {
|
|
303
|
+
lines.push(item.summary);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
lines.push("");
|
|
309
|
+
lines.push("当前统计:");
|
|
310
|
+
lines.push(`- 已完成:${summary.done}`);
|
|
311
|
+
lines.push(`- 待处理:${summary.pending}`);
|
|
312
|
+
lines.push(`- 进行中:${summary.inProgress}`);
|
|
313
|
+
lines.push(`- 阻塞:${summary.blocked}`);
|
|
314
|
+
lines.push(`- 失败:${summary.failed}`);
|
|
315
|
+
lines.push(`- 状态文件:${context.statusFile.replaceAll("\\", "/")}`);
|
|
316
|
+
lines.push("");
|
|
317
|
+
lines.push("结论:");
|
|
318
|
+
if (execution.state === "done") {
|
|
319
|
+
lines.push("- backlog 已全部完成");
|
|
320
|
+
} else if (execution.blockedReason) {
|
|
321
|
+
lines.push(`- 当前停止原因:${execution.blockedReason}`);
|
|
322
|
+
} else {
|
|
323
|
+
lines.push("- 当前轮次已结束,可继续查看 status 或下一任务");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return lines.join("\n");
|
|
327
|
+
}
|
package/src/install.mjs
CHANGED
|
@@ -3,16 +3,18 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
|
-
import { ensureDir, fileExists, readJson, writeJson } from "./common.mjs";
|
|
6
|
+
import { ensureDir, fileExists, nowIso, readJson, writeJson } from "./common.mjs";
|
|
7
7
|
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
9
|
const __dirname = path.dirname(__filename);
|
|
10
10
|
|
|
11
11
|
export const runtimeBundleEntries = [
|
|
12
|
+
".claude-plugin",
|
|
12
13
|
".codex-plugin",
|
|
13
14
|
"LICENSE",
|
|
14
15
|
"README.md",
|
|
15
16
|
"bin",
|
|
17
|
+
"hosts",
|
|
16
18
|
"package.json",
|
|
17
19
|
"scripts",
|
|
18
20
|
"skills",
|
|
@@ -20,8 +22,17 @@ export const runtimeBundleEntries = [
|
|
|
20
22
|
"templates",
|
|
21
23
|
];
|
|
22
24
|
|
|
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));
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
function assertPathInside(parentDir, targetDir, label) {
|
|
@@ -31,21 +42,38 @@ function assertPathInside(parentDir, targetDir, label) {
|
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
44
|
|
|
34
|
-
function
|
|
35
|
-
|
|
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 copyBundleEntries(bundleRoot, targetRoot, entries) {
|
|
56
|
+
for (const entry of entries) {
|
|
36
57
|
const sourcePath = path.join(bundleRoot, entry);
|
|
37
58
|
if (!fileExists(sourcePath)) {
|
|
38
59
|
continue;
|
|
39
60
|
}
|
|
40
61
|
|
|
41
|
-
fs.cpSync(sourcePath, path.join(
|
|
62
|
+
fs.cpSync(sourcePath, path.join(targetRoot, entry), {
|
|
42
63
|
force: true,
|
|
43
64
|
recursive: true,
|
|
44
65
|
});
|
|
45
66
|
}
|
|
46
67
|
}
|
|
47
68
|
|
|
48
|
-
function
|
|
69
|
+
function copyDirectory(sourceRoot, targetRoot) {
|
|
70
|
+
fs.cpSync(sourceRoot, targetRoot, {
|
|
71
|
+
force: true,
|
|
72
|
+
recursive: true,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function updateCodexMarketplace(marketplaceFile) {
|
|
49
77
|
const marketplace = fileExists(marketplaceFile)
|
|
50
78
|
? readJson(marketplaceFile)
|
|
51
79
|
: {
|
|
@@ -84,30 +112,82 @@ function updateMarketplace(marketplaceFile) {
|
|
|
84
112
|
writeJson(marketplaceFile, marketplace);
|
|
85
113
|
}
|
|
86
114
|
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
115
|
+
function updateClaudeSettings(settingsFile, marketplaceRoot) {
|
|
116
|
+
const settings = fileExists(settingsFile)
|
|
117
|
+
? readJson(settingsFile)
|
|
118
|
+
: {};
|
|
119
|
+
|
|
120
|
+
settings.extraKnownMarketplaces = settings.extraKnownMarketplaces || {};
|
|
121
|
+
settings.enabledPlugins = settings.enabledPlugins || {};
|
|
122
|
+
|
|
123
|
+
settings.extraKnownMarketplaces[CLAUDE_MARKETPLACE_NAME] = {
|
|
124
|
+
source: {
|
|
125
|
+
source: "directory",
|
|
126
|
+
path: marketplaceRoot,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
settings.enabledPlugins[CLAUDE_PLUGIN_KEY] = true;
|
|
130
|
+
|
|
131
|
+
writeJson(settingsFile, settings);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function updateClaudeKnownMarketplaces(knownMarketplacesFile, marketplaceRoot, updatedAt) {
|
|
135
|
+
const knownMarketplaces = fileExists(knownMarketplacesFile)
|
|
136
|
+
? readJson(knownMarketplacesFile)
|
|
137
|
+
: {};
|
|
138
|
+
|
|
139
|
+
knownMarketplaces[CLAUDE_MARKETPLACE_NAME] = {
|
|
140
|
+
source: {
|
|
141
|
+
source: "directory",
|
|
142
|
+
path: marketplaceRoot,
|
|
143
|
+
},
|
|
144
|
+
installLocation: marketplaceRoot,
|
|
145
|
+
lastUpdated: updatedAt,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
writeJson(knownMarketplacesFile, knownMarketplaces);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function updateClaudeInstalledPlugins(installedPluginsFile, pluginRoot, pluginVersion, updatedAt) {
|
|
152
|
+
const installedPlugins = fileExists(installedPluginsFile)
|
|
153
|
+
? readJson(installedPluginsFile)
|
|
154
|
+
: {
|
|
155
|
+
version: 2,
|
|
156
|
+
plugins: {},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
installedPlugins.version = 2;
|
|
160
|
+
installedPlugins.plugins = installedPlugins.plugins || {};
|
|
161
|
+
installedPlugins.plugins[CLAUDE_PLUGIN_KEY] = [
|
|
162
|
+
{
|
|
163
|
+
scope: "user",
|
|
164
|
+
installPath: pluginRoot,
|
|
165
|
+
version: pluginVersion,
|
|
166
|
+
installedAt: updatedAt,
|
|
167
|
+
lastUpdated: updatedAt,
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
writeJson(installedPluginsFile, installedPlugins);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function installCodexHost(bundleRoot, options) {
|
|
175
|
+
const resolvedCodexHome = resolveHomeDir(options.codexHome, ".codex");
|
|
90
176
|
const targetPluginsRoot = path.join(resolvedCodexHome, "plugins");
|
|
91
177
|
const targetPluginRoot = path.join(targetPluginsRoot, "helloloop");
|
|
92
178
|
const marketplaceFile = path.join(resolvedCodexHome, ".agents", "plugins", "marketplace.json");
|
|
93
179
|
const manifestFile = path.join(bundleRoot, ".codex-plugin", "plugin.json");
|
|
94
180
|
|
|
95
181
|
if (!fileExists(manifestFile)) {
|
|
96
|
-
throw new Error(
|
|
182
|
+
throw new Error(`未找到 Codex 插件 manifest:${manifestFile}`);
|
|
97
183
|
}
|
|
98
184
|
|
|
99
|
-
assertPathInside(resolvedCodexHome, targetPluginRoot, "目标插件目录");
|
|
100
|
-
|
|
101
|
-
if (fileExists(targetPluginRoot)) {
|
|
102
|
-
if (!options.force) {
|
|
103
|
-
throw new Error(`目标插件目录已存在:${targetPluginRoot}。若要覆盖,请追加 --force。`);
|
|
104
|
-
}
|
|
105
|
-
fs.rmSync(targetPluginRoot, { recursive: true, force: true });
|
|
106
|
-
}
|
|
185
|
+
assertPathInside(resolvedCodexHome, targetPluginRoot, "Codex 目标插件目录");
|
|
186
|
+
removeTargetIfNeeded(targetPluginRoot, options.force);
|
|
107
187
|
|
|
108
188
|
ensureDir(targetPluginsRoot);
|
|
109
189
|
ensureDir(targetPluginRoot);
|
|
110
|
-
copyBundleEntries(bundleRoot, targetPluginRoot);
|
|
190
|
+
copyBundleEntries(bundleRoot, targetPluginRoot, codexBundleEntries);
|
|
111
191
|
|
|
112
192
|
const gitMetadataPath = path.join(targetPluginRoot, ".git");
|
|
113
193
|
if (fileExists(gitMetadataPath)) {
|
|
@@ -115,10 +195,108 @@ export function installPluginBundle(options = {}) {
|
|
|
115
195
|
}
|
|
116
196
|
|
|
117
197
|
ensureDir(path.dirname(marketplaceFile));
|
|
118
|
-
|
|
198
|
+
updateCodexMarketplace(marketplaceFile);
|
|
119
199
|
|
|
120
200
|
return {
|
|
121
|
-
|
|
201
|
+
host: "codex",
|
|
202
|
+
displayName: "Codex",
|
|
203
|
+
targetRoot: targetPluginRoot,
|
|
122
204
|
marketplaceFile,
|
|
123
205
|
};
|
|
124
206
|
}
|
|
207
|
+
|
|
208
|
+
function installClaudeHost(bundleRoot, options) {
|
|
209
|
+
const resolvedClaudeHome = resolveHomeDir(options.claudeHome, ".claude");
|
|
210
|
+
const sourceMarketplaceRoot = path.join(bundleRoot, "hosts", "claude", "marketplace");
|
|
211
|
+
const sourceManifest = path.join(bundleRoot, ".claude-plugin", "plugin.json");
|
|
212
|
+
const pluginVersion = readJson(sourceManifest).version || readJson(path.join(bundleRoot, "package.json")).version;
|
|
213
|
+
const targetPluginsRoot = path.join(resolvedClaudeHome, "plugins");
|
|
214
|
+
const targetMarketplaceRoot = path.join(targetPluginsRoot, "marketplaces", CLAUDE_MARKETPLACE_NAME);
|
|
215
|
+
const targetCachePluginsRoot = path.join(targetPluginsRoot, "cache", CLAUDE_MARKETPLACE_NAME, "helloloop");
|
|
216
|
+
const targetInstalledPluginRoot = path.join(targetCachePluginsRoot, pluginVersion);
|
|
217
|
+
const knownMarketplacesFile = path.join(targetPluginsRoot, "known_marketplaces.json");
|
|
218
|
+
const installedPluginsFile = path.join(targetPluginsRoot, "installed_plugins.json");
|
|
219
|
+
const settingsFile = path.join(resolvedClaudeHome, "settings.json");
|
|
220
|
+
|
|
221
|
+
if (!fileExists(sourceManifest)) {
|
|
222
|
+
throw new Error(`未找到 Claude 插件 manifest:${sourceManifest}`);
|
|
223
|
+
}
|
|
224
|
+
if (!fileExists(path.join(sourceMarketplaceRoot, ".claude-plugin", "marketplace.json"))) {
|
|
225
|
+
throw new Error(`未找到 Claude marketplace 模板:${sourceMarketplaceRoot}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
assertPathInside(resolvedClaudeHome, targetMarketplaceRoot, "Claude marketplace 目录");
|
|
229
|
+
assertPathInside(resolvedClaudeHome, targetInstalledPluginRoot, "Claude 插件缓存目录");
|
|
230
|
+
removeTargetIfNeeded(targetMarketplaceRoot, options.force);
|
|
231
|
+
removeTargetIfNeeded(targetCachePluginsRoot, options.force);
|
|
232
|
+
|
|
233
|
+
ensureDir(path.dirname(targetMarketplaceRoot));
|
|
234
|
+
ensureDir(path.dirname(targetInstalledPluginRoot));
|
|
235
|
+
copyDirectory(sourceMarketplaceRoot, targetMarketplaceRoot);
|
|
236
|
+
copyDirectory(path.join(sourceMarketplaceRoot, "plugins", "helloloop"), targetInstalledPluginRoot);
|
|
237
|
+
ensureDir(targetPluginsRoot);
|
|
238
|
+
const updatedAt = nowIso();
|
|
239
|
+
updateClaudeSettings(settingsFile, targetMarketplaceRoot);
|
|
240
|
+
updateClaudeKnownMarketplaces(knownMarketplacesFile, targetMarketplaceRoot, updatedAt);
|
|
241
|
+
updateClaudeInstalledPlugins(installedPluginsFile, targetInstalledPluginRoot, pluginVersion, updatedAt);
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
host: "claude",
|
|
245
|
+
displayName: "Claude",
|
|
246
|
+
targetRoot: targetInstalledPluginRoot,
|
|
247
|
+
marketplaceFile: path.join(targetMarketplaceRoot, ".claude-plugin", "marketplace.json"),
|
|
248
|
+
settingsFile,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function installGeminiHost(bundleRoot, options) {
|
|
253
|
+
const resolvedGeminiHome = resolveHomeDir(options.geminiHome, ".gemini");
|
|
254
|
+
const sourceExtensionRoot = path.join(bundleRoot, "hosts", "gemini", "extension");
|
|
255
|
+
const targetExtensionRoot = path.join(resolvedGeminiHome, "extensions", "helloloop");
|
|
256
|
+
|
|
257
|
+
if (!fileExists(path.join(sourceExtensionRoot, "gemini-extension.json"))) {
|
|
258
|
+
throw new Error(`未找到 Gemini 扩展清单:${sourceExtensionRoot}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
assertPathInside(resolvedGeminiHome, targetExtensionRoot, "Gemini 扩展目录");
|
|
262
|
+
removeTargetIfNeeded(targetExtensionRoot, options.force);
|
|
263
|
+
|
|
264
|
+
ensureDir(path.dirname(targetExtensionRoot));
|
|
265
|
+
copyDirectory(sourceExtensionRoot, targetExtensionRoot);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
host: "gemini",
|
|
269
|
+
displayName: "Gemini",
|
|
270
|
+
targetRoot: targetExtensionRoot,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function resolveInstallHosts(hostOption) {
|
|
275
|
+
const normalized = String(hostOption || "codex").trim().toLowerCase();
|
|
276
|
+
if (normalized === "all") {
|
|
277
|
+
return [...supportedHosts];
|
|
278
|
+
}
|
|
279
|
+
if (!supportedHosts.includes(normalized)) {
|
|
280
|
+
throw new Error(`不支持的宿主:${hostOption}。可选值:codex、claude、gemini、all`);
|
|
281
|
+
}
|
|
282
|
+
return [normalized];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function installPluginBundle(options = {}) {
|
|
286
|
+
const bundleRoot = path.resolve(options.bundleRoot || path.join(__dirname, ".."));
|
|
287
|
+
const selectedHosts = resolveInstallHosts(options.host);
|
|
288
|
+
const installers = {
|
|
289
|
+
codex: () => installCodexHost(bundleRoot, options),
|
|
290
|
+
claude: () => installClaudeHost(bundleRoot, options),
|
|
291
|
+
gemini: () => installGeminiHost(bundleRoot, options),
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const installedHosts = selectedHosts.map((host) => installers[host]());
|
|
295
|
+
const codexResult = installedHosts.find((item) => item.host === "codex");
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
installedHosts,
|
|
299
|
+
targetPluginRoot: codexResult?.targetRoot || "",
|
|
300
|
+
marketplaceFile: codexResult?.marketplaceFile || "",
|
|
301
|
+
};
|
|
302
|
+
}
|
package/src/shell_invocation.mjs
CHANGED
|
@@ -125,7 +125,7 @@ function isCmdLikeExecutable(executable) {
|
|
|
125
125
|
return /\.(cmd|bat)$/i.test(String(executable || ""));
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
function
|
|
128
|
+
function resolveWindowsNamedExecutable(toolName, options = {}) {
|
|
129
129
|
const explicitExecutable = String(options.explicitExecutable || "").trim();
|
|
130
130
|
const findCommandPaths = options.findCommandPaths || ((command) => findWindowsCommandPaths(command));
|
|
131
131
|
|
|
@@ -133,7 +133,7 @@ function resolveWindowsCodexExecutable(options = {}) {
|
|
|
133
133
|
return explicitExecutable;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
const searchOrder = [
|
|
136
|
+
const searchOrder = [`${toolName}.ps1`, `${toolName}.exe`, toolName];
|
|
137
137
|
for (const query of searchOrder) {
|
|
138
138
|
const safeMatch = findCommandPaths(query).find((candidate) => !isCmdLikeExecutable(candidate));
|
|
139
139
|
if (safeMatch) {
|
|
@@ -160,31 +160,37 @@ export function resolveVerifyShellInvocation(options = {}) {
|
|
|
160
160
|
return resolvePosixCommandShell(options);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
export function
|
|
163
|
+
export function resolveCliInvocation(options = {}) {
|
|
164
164
|
const platform = options.platform || process.platform;
|
|
165
165
|
const explicitExecutable = String(options.explicitExecutable || "").trim();
|
|
166
|
+
const commandName = String(options.commandName || "").trim();
|
|
167
|
+
const toolDisplayName = String(options.toolDisplayName || commandName || "CLI");
|
|
168
|
+
|
|
169
|
+
if (!commandName && !explicitExecutable) {
|
|
170
|
+
return createUnavailableInvocation("未提供 CLI 名称或显式可执行路径。");
|
|
171
|
+
}
|
|
166
172
|
|
|
167
173
|
if (platform !== "win32") {
|
|
168
174
|
return {
|
|
169
|
-
command: explicitExecutable ||
|
|
175
|
+
command: explicitExecutable || commandName,
|
|
170
176
|
argsPrefix: [],
|
|
171
177
|
shell: false,
|
|
172
178
|
};
|
|
173
179
|
}
|
|
174
180
|
|
|
175
|
-
const executable =
|
|
181
|
+
const executable = resolveWindowsNamedExecutable(commandName, {
|
|
176
182
|
explicitExecutable,
|
|
177
183
|
findCommandPaths: options.findCommandPaths,
|
|
178
184
|
});
|
|
179
185
|
if (!executable) {
|
|
180
186
|
return createUnavailableInvocation(
|
|
181
|
-
|
|
187
|
+
`未找到可安全执行的 ${toolDisplayName} 入口。Windows 环境需要 ${commandName}.ps1、${commandName}.exe 或其他非 cmd 的可执行入口。`,
|
|
182
188
|
);
|
|
183
189
|
}
|
|
184
190
|
|
|
185
191
|
if (isCmdLikeExecutable(executable)) {
|
|
186
192
|
return createUnavailableInvocation(
|
|
187
|
-
`HelloLoop 在 Windows 已禁止通过 cmd/bat 启动
|
|
193
|
+
`HelloLoop 在 Windows 已禁止通过 cmd/bat 启动 ${toolDisplayName}:${executable}`,
|
|
188
194
|
);
|
|
189
195
|
}
|
|
190
196
|
|
|
@@ -209,3 +215,11 @@ export function resolveCodexInvocation(options = {}) {
|
|
|
209
215
|
shell: false,
|
|
210
216
|
};
|
|
211
217
|
}
|
|
218
|
+
|
|
219
|
+
export function resolveCodexInvocation(options = {}) {
|
|
220
|
+
return resolveCliInvocation({
|
|
221
|
+
...options,
|
|
222
|
+
commandName: "codex",
|
|
223
|
+
toolDisplayName: "Codex",
|
|
224
|
+
});
|
|
225
|
+
}
|