helloloop 0.2.0 → 0.3.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 +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +228 -279
- package/hosts/claude/marketplace/.claude-plugin/marketplace.json +3 -1
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +16 -9
- package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +4 -1
- package/hosts/gemini/extension/GEMINI.md +8 -4
- package/hosts/gemini/extension/commands/helloloop.toml +15 -8
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/scripts/uninstall-home-plugin.ps1 +25 -0
- package/skills/helloloop/SKILL.md +28 -3
- package/src/analyze_confirmation.mjs +79 -3
- package/src/analyze_prompt.mjs +12 -0
- package/src/analyze_user_input.mjs +303 -0
- package/src/analyzer.mjs +38 -0
- package/src/cli.mjs +211 -25
- package/src/cli_support.mjs +114 -27
- package/src/discovery.mjs +243 -9
- package/src/discovery_inference.mjs +62 -18
- package/src/discovery_paths.mjs +143 -8
- package/src/discovery_prompt.mjs +298 -0
- package/src/install.mjs +218 -7
- package/src/rebuild.mjs +116 -0
- package/templates/analysis-output.schema.json +58 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
listDocFilesInDirectory,
|
|
7
|
+
listProjectCandidatesInDirectory,
|
|
8
|
+
pathExists,
|
|
9
|
+
resolveAbsolute,
|
|
10
|
+
} from "./discovery_paths.mjs";
|
|
11
|
+
|
|
12
|
+
function toDisplayPath(targetPath) {
|
|
13
|
+
return String(targetPath || "").replaceAll("\\", "/");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createDiscoveryPromptSession() {
|
|
17
|
+
if (process.stdin.isTTY) {
|
|
18
|
+
const readline = createInterface({
|
|
19
|
+
input: process.stdin,
|
|
20
|
+
output: process.stdout,
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
async question(promptText) {
|
|
24
|
+
return readline.question(promptText);
|
|
25
|
+
},
|
|
26
|
+
close() {
|
|
27
|
+
readline.close();
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const bufferedAnswers = fs.readFileSync(0, "utf8").split(/\r?\n/);
|
|
33
|
+
let answerIndex = 0;
|
|
34
|
+
return {
|
|
35
|
+
async question(promptText) {
|
|
36
|
+
process.stdout.write(promptText);
|
|
37
|
+
const answer = bufferedAnswers[answerIndex] ?? "";
|
|
38
|
+
answerIndex += 1;
|
|
39
|
+
return answer;
|
|
40
|
+
},
|
|
41
|
+
close() {},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function summarizeList(items, options = {}) {
|
|
46
|
+
const limit = Number(options.limit || 12);
|
|
47
|
+
if (!items.length) {
|
|
48
|
+
return ["- 无"];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const visible = items.slice(0, limit);
|
|
52
|
+
const lines = visible.map((item) => `- ${item}`);
|
|
53
|
+
if (items.length > limit) {
|
|
54
|
+
lines.push(`- 其余 ${items.length - limit} 项未展开`);
|
|
55
|
+
}
|
|
56
|
+
return lines;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function collectDirectoryOverview(rootPath) {
|
|
60
|
+
if (!pathExists(rootPath) || !fs.statSync(rootPath).isDirectory()) {
|
|
61
|
+
return {
|
|
62
|
+
rootPath,
|
|
63
|
+
directories: [],
|
|
64
|
+
docFiles: [],
|
|
65
|
+
repoCandidates: [],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const entries = fs.readdirSync(rootPath, { withFileTypes: true });
|
|
70
|
+
return {
|
|
71
|
+
rootPath,
|
|
72
|
+
directories: entries
|
|
73
|
+
.filter((entry) => entry.isDirectory())
|
|
74
|
+
.map((entry) => entry.name)
|
|
75
|
+
.sort((left, right) => left.localeCompare(right, "zh-CN")),
|
|
76
|
+
docFiles: listDocFilesInDirectory(rootPath)
|
|
77
|
+
.map((filePath) => path.basename(filePath))
|
|
78
|
+
.sort((left, right) => left.localeCompare(right, "zh-CN")),
|
|
79
|
+
repoCandidates: listProjectCandidatesInDirectory(rootPath)
|
|
80
|
+
.map((directoryPath) => path.basename(directoryPath))
|
|
81
|
+
.sort((left, right) => left.localeCompare(right, "zh-CN")),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderDirectoryOverview(title, overview) {
|
|
86
|
+
return [
|
|
87
|
+
title,
|
|
88
|
+
`扫描目录:${toDisplayPath(overview.rootPath)}`,
|
|
89
|
+
"",
|
|
90
|
+
"顶层文档文件:",
|
|
91
|
+
...summarizeList(overview.docFiles),
|
|
92
|
+
"",
|
|
93
|
+
"顶层目录:",
|
|
94
|
+
...summarizeList(overview.directories),
|
|
95
|
+
"",
|
|
96
|
+
"疑似项目目录:",
|
|
97
|
+
...summarizeList(overview.repoCandidates),
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function renderExistingChoices(title, candidates) {
|
|
102
|
+
return [
|
|
103
|
+
title,
|
|
104
|
+
...candidates.map((item, index) => `${index + 1}. ${toDisplayPath(item)}`),
|
|
105
|
+
"",
|
|
106
|
+
"请输入编号;也可以直接输入本地路径;直接回车取消。",
|
|
107
|
+
].join("\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function promptForExistingPathSelection(readline, title, candidates, cwd, preface = "") {
|
|
111
|
+
if (preface) {
|
|
112
|
+
console.log(preface);
|
|
113
|
+
console.log("");
|
|
114
|
+
}
|
|
115
|
+
console.log(renderExistingChoices(title, candidates));
|
|
116
|
+
while (true) {
|
|
117
|
+
const answer = String(await readline.question("> ") || "").trim();
|
|
118
|
+
if (!answer) {
|
|
119
|
+
return "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const choiceIndex = Number(answer);
|
|
123
|
+
if (Number.isInteger(choiceIndex) && choiceIndex >= 1 && choiceIndex <= candidates.length) {
|
|
124
|
+
return candidates[choiceIndex - 1];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const maybePath = resolveAbsolute(answer, cwd);
|
|
128
|
+
if (pathExists(maybePath)) {
|
|
129
|
+
return maybePath;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log("输入无效,请输入候选编号或一个存在的本地路径。");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function promptForDocsPath(readline, discovery, cwd) {
|
|
137
|
+
const docChoices = Array.isArray(discovery.docCandidates) ? discovery.docCandidates : [];
|
|
138
|
+
const scanRoot = discovery.workspaceRoot || discovery.repoRoot || cwd;
|
|
139
|
+
const overview = collectDirectoryOverview(scanRoot);
|
|
140
|
+
const title = docChoices.length
|
|
141
|
+
? "请选择开发文档来源:"
|
|
142
|
+
: "未自动识别到明确的开发文档。请输入开发文档目录或文件路径:";
|
|
143
|
+
const preface = renderDirectoryOverview("当前目录顶层概览", overview);
|
|
144
|
+
|
|
145
|
+
if (docChoices.length) {
|
|
146
|
+
return promptForExistingPathSelection(readline, title, docChoices, cwd, preface);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(preface);
|
|
150
|
+
console.log("");
|
|
151
|
+
console.log(title);
|
|
152
|
+
console.log("可直接输入当前目录下的相对路径或绝对路径;直接回车取消。");
|
|
153
|
+
while (true) {
|
|
154
|
+
const answer = String(await readline.question("> ") || "").trim();
|
|
155
|
+
if (!answer) {
|
|
156
|
+
return "";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const maybePath = resolveAbsolute(answer, cwd);
|
|
160
|
+
if (pathExists(maybePath)) {
|
|
161
|
+
return maybePath;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log("路径不存在,请输入一个已存在的开发文档目录或文件路径。");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function promptForRepoPath(readline, discovery, cwd) {
|
|
169
|
+
const repoChoices = Array.isArray(discovery.repoCandidates) ? discovery.repoCandidates : [];
|
|
170
|
+
const scanRoot = discovery.workspaceRoot
|
|
171
|
+
|| (Array.isArray(discovery.docsEntries) && discovery.docsEntries.length
|
|
172
|
+
? path.dirname(discovery.docsEntries[0])
|
|
173
|
+
: "")
|
|
174
|
+
|| cwd;
|
|
175
|
+
const overview = collectDirectoryOverview(scanRoot);
|
|
176
|
+
const preface = renderDirectoryOverview("当前目录顶层概览", overview);
|
|
177
|
+
const title = repoChoices.length
|
|
178
|
+
? "请选择目标项目仓库:"
|
|
179
|
+
: "请输入要开发的项目路径:";
|
|
180
|
+
console.log(preface);
|
|
181
|
+
console.log("");
|
|
182
|
+
if (repoChoices.length) {
|
|
183
|
+
console.log(renderExistingChoices(title, repoChoices));
|
|
184
|
+
console.log("也可以直接输入项目路径;如果这是新项目,可输入准备创建的新目录路径。");
|
|
185
|
+
} else {
|
|
186
|
+
console.log(title);
|
|
187
|
+
console.log("如果这是新项目,可直接输入准备创建的新目录路径;直接回车取消。");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
while (true) {
|
|
191
|
+
const answer = String(await readline.question("> ") || "").trim();
|
|
192
|
+
if (!answer) {
|
|
193
|
+
return { repoRoot: "", allowNewRepoRoot: false };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const choiceIndex = Number(answer);
|
|
197
|
+
if (repoChoices.length && Number.isInteger(choiceIndex) && choiceIndex >= 1 && choiceIndex <= repoChoices.length) {
|
|
198
|
+
return {
|
|
199
|
+
repoRoot: repoChoices[choiceIndex - 1],
|
|
200
|
+
allowNewRepoRoot: false,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const maybePath = resolveAbsolute(answer, cwd);
|
|
205
|
+
if (pathExists(maybePath) && !fs.statSync(maybePath).isDirectory()) {
|
|
206
|
+
console.log("项目路径必须是目录,不能是文件。");
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
repoRoot: maybePath,
|
|
212
|
+
allowNewRepoRoot: !pathExists(maybePath),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function resolveDiscoveryFailureInteractively(
|
|
218
|
+
failure,
|
|
219
|
+
options = {},
|
|
220
|
+
cwd = process.cwd(),
|
|
221
|
+
allowPrompt = true,
|
|
222
|
+
sharedPromptSession = null,
|
|
223
|
+
) {
|
|
224
|
+
const discovery = failure?.discovery || {};
|
|
225
|
+
const nextOptions = {
|
|
226
|
+
...options,
|
|
227
|
+
selectionSources: {
|
|
228
|
+
...(options.selectionSources || {}),
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
let changed = false;
|
|
232
|
+
const promptSession = sharedPromptSession || (allowPrompt ? createDiscoveryPromptSession() : null);
|
|
233
|
+
const ownsPromptSession = Boolean(promptSession) && !sharedPromptSession;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
if (!nextOptions.docsPath) {
|
|
237
|
+
const docChoices = Array.isArray(discovery.docCandidates) ? discovery.docCandidates : [];
|
|
238
|
+
if (Array.isArray(discovery.docsEntries) && discovery.docsEntries.length === 1) {
|
|
239
|
+
nextOptions.docsPath = discovery.docsEntries[0];
|
|
240
|
+
nextOptions.selectionSources.docs = "workspace_single_doc";
|
|
241
|
+
changed = true;
|
|
242
|
+
} else if (docChoices.length === 1) {
|
|
243
|
+
nextOptions.docsPath = docChoices[0];
|
|
244
|
+
nextOptions.selectionSources.docs = "workspace_single_doc";
|
|
245
|
+
changed = true;
|
|
246
|
+
} else if (failure?.code === "missing_docs" || docChoices.length > 1 || discovery.workspaceRoot) {
|
|
247
|
+
if (!allowPrompt) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
const selectedDocs = await promptForDocsPath(promptSession, discovery, cwd);
|
|
251
|
+
if (!selectedDocs) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
nextOptions.docsPath = selectedDocs;
|
|
255
|
+
nextOptions.selectionSources.docs = "interactive";
|
|
256
|
+
changed = true;
|
|
257
|
+
console.log(`已选择开发文档:${toDisplayPath(selectedDocs)}`);
|
|
258
|
+
console.log("");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const repoChoices = Array.isArray(discovery.repoCandidates) ? discovery.repoCandidates : [];
|
|
263
|
+
if (!nextOptions.repoRoot && repoChoices.length) {
|
|
264
|
+
if (repoChoices.length === 1) {
|
|
265
|
+
nextOptions.repoRoot = repoChoices[0];
|
|
266
|
+
nextOptions.selectionSources.repo = "workspace_single_repo";
|
|
267
|
+
changed = true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!nextOptions.repoRoot && failure?.code === "missing_repo") {
|
|
272
|
+
if (!allowPrompt) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const selectedRepo = await promptForRepoPath(promptSession, discovery, cwd);
|
|
276
|
+
if (!selectedRepo.repoRoot) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
nextOptions.repoRoot = selectedRepo.repoRoot;
|
|
280
|
+
nextOptions.allowNewRepoRoot = selectedRepo.allowNewRepoRoot;
|
|
281
|
+
nextOptions.selectionSources.repo = selectedRepo.allowNewRepoRoot
|
|
282
|
+
? "interactive_new_repo"
|
|
283
|
+
: "interactive";
|
|
284
|
+
changed = true;
|
|
285
|
+
if (selectedRepo.allowNewRepoRoot) {
|
|
286
|
+
console.log(`已指定项目路径(当前不存在,将按新项目创建):${toDisplayPath(selectedRepo.repoRoot)}`);
|
|
287
|
+
} else {
|
|
288
|
+
console.log(`已选择项目仓库:${toDisplayPath(selectedRepo.repoRoot)}`);
|
|
289
|
+
}
|
|
290
|
+
console.log("");
|
|
291
|
+
}
|
|
292
|
+
return changed ? nextOptions : null;
|
|
293
|
+
} finally {
|
|
294
|
+
if (ownsPromptSession) {
|
|
295
|
+
promptSession?.close();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
package/src/install.mjs
CHANGED
|
@@ -3,7 +3,7 @@ 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);
|
|
@@ -28,6 +28,8 @@ const codexBundleEntries = runtimeBundleEntries.filter((entry) => ![
|
|
|
28
28
|
].includes(entry));
|
|
29
29
|
|
|
30
30
|
const supportedHosts = ["codex", "claude", "gemini"];
|
|
31
|
+
const CLAUDE_MARKETPLACE_NAME = "helloloop-local";
|
|
32
|
+
const CLAUDE_PLUGIN_KEY = "helloloop@helloloop-local";
|
|
31
33
|
|
|
32
34
|
function resolveHomeDir(homeDir, defaultDirName) {
|
|
33
35
|
return path.resolve(homeDir || path.join(os.homedir(), defaultDirName));
|
|
@@ -50,6 +52,14 @@ function removeTargetIfNeeded(targetPath, force) {
|
|
|
50
52
|
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
51
53
|
}
|
|
52
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
|
+
|
|
53
63
|
function copyBundleEntries(bundleRoot, targetRoot, entries) {
|
|
54
64
|
for (const entry of entries) {
|
|
55
65
|
const sourcePath = path.join(bundleRoot, entry);
|
|
@@ -110,6 +120,23 @@ function updateCodexMarketplace(marketplaceFile) {
|
|
|
110
120
|
writeJson(marketplaceFile, marketplace);
|
|
111
121
|
}
|
|
112
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
|
+
|
|
113
140
|
function updateClaudeSettings(settingsFile, marketplaceRoot) {
|
|
114
141
|
const settings = fileExists(settingsFile)
|
|
115
142
|
? readJson(settingsFile)
|
|
@@ -118,15 +145,111 @@ function updateClaudeSettings(settingsFile, marketplaceRoot) {
|
|
|
118
145
|
settings.extraKnownMarketplaces = settings.extraKnownMarketplaces || {};
|
|
119
146
|
settings.enabledPlugins = settings.enabledPlugins || {};
|
|
120
147
|
|
|
121
|
-
settings.extraKnownMarketplaces[
|
|
122
|
-
source:
|
|
123
|
-
|
|
148
|
+
settings.extraKnownMarketplaces[CLAUDE_MARKETPLACE_NAME] = {
|
|
149
|
+
source: {
|
|
150
|
+
source: "directory",
|
|
151
|
+
path: marketplaceRoot,
|
|
152
|
+
},
|
|
124
153
|
};
|
|
125
|
-
settings.enabledPlugins[
|
|
154
|
+
settings.enabledPlugins[CLAUDE_PLUGIN_KEY] = true;
|
|
126
155
|
|
|
127
156
|
writeJson(settingsFile, settings);
|
|
128
157
|
}
|
|
129
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
|
+
|
|
130
253
|
function installCodexHost(bundleRoot, options) {
|
|
131
254
|
const resolvedCodexHome = resolveHomeDir(options.codexHome, ".codex");
|
|
132
255
|
const targetPluginsRoot = path.join(resolvedCodexHome, "plugins");
|
|
@@ -161,11 +284,34 @@ function installCodexHost(bundleRoot, options) {
|
|
|
161
284
|
};
|
|
162
285
|
}
|
|
163
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
|
+
|
|
164
304
|
function installClaudeHost(bundleRoot, options) {
|
|
165
305
|
const resolvedClaudeHome = resolveHomeDir(options.claudeHome, ".claude");
|
|
166
306
|
const sourceMarketplaceRoot = path.join(bundleRoot, "hosts", "claude", "marketplace");
|
|
167
307
|
const sourceManifest = path.join(bundleRoot, ".claude-plugin", "plugin.json");
|
|
168
|
-
const
|
|
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");
|
|
169
315
|
const settingsFile = path.join(resolvedClaudeHome, "settings.json");
|
|
170
316
|
|
|
171
317
|
if (!fileExists(sourceManifest)) {
|
|
@@ -176,16 +322,55 @@ function installClaudeHost(bundleRoot, options) {
|
|
|
176
322
|
}
|
|
177
323
|
|
|
178
324
|
assertPathInside(resolvedClaudeHome, targetMarketplaceRoot, "Claude marketplace 目录");
|
|
325
|
+
assertPathInside(resolvedClaudeHome, targetInstalledPluginRoot, "Claude 插件缓存目录");
|
|
179
326
|
removeTargetIfNeeded(targetMarketplaceRoot, options.force);
|
|
327
|
+
removeTargetIfNeeded(targetCachePluginsRoot, options.force);
|
|
180
328
|
|
|
181
329
|
ensureDir(path.dirname(targetMarketplaceRoot));
|
|
330
|
+
ensureDir(path.dirname(targetInstalledPluginRoot));
|
|
182
331
|
copyDirectory(sourceMarketplaceRoot, targetMarketplaceRoot);
|
|
332
|
+
copyDirectory(path.join(sourceMarketplaceRoot, "plugins", "helloloop"), targetInstalledPluginRoot);
|
|
333
|
+
ensureDir(targetPluginsRoot);
|
|
334
|
+
const updatedAt = nowIso();
|
|
183
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);
|
|
184
362
|
|
|
185
363
|
return {
|
|
186
364
|
host: "claude",
|
|
187
365
|
displayName: "Claude",
|
|
188
|
-
targetRoot:
|
|
366
|
+
targetRoot: targetCachePluginsRoot,
|
|
367
|
+
removed: [
|
|
368
|
+
removedMarketplaceDir,
|
|
369
|
+
removedCacheDir,
|
|
370
|
+
removedKnownMarketplace,
|
|
371
|
+
removedInstalledPlugin,
|
|
372
|
+
removedSettingsEntries,
|
|
373
|
+
].some(Boolean),
|
|
189
374
|
marketplaceFile: path.join(targetMarketplaceRoot, ".claude-plugin", "marketplace.json"),
|
|
190
375
|
settingsFile,
|
|
191
376
|
};
|
|
@@ -213,6 +398,18 @@ function installGeminiHost(bundleRoot, options) {
|
|
|
213
398
|
};
|
|
214
399
|
}
|
|
215
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
|
+
|
|
216
413
|
function resolveInstallHosts(hostOption) {
|
|
217
414
|
const normalized = String(hostOption || "codex").trim().toLowerCase();
|
|
218
415
|
if (normalized === "all") {
|
|
@@ -242,3 +439,17 @@ export function installPluginBundle(options = {}) {
|
|
|
242
439
|
marketplaceFile: codexResult?.marketplaceFile || "",
|
|
243
440
|
};
|
|
244
441
|
}
|
|
442
|
+
|
|
443
|
+
export function uninstallPluginBundle(options = {}) {
|
|
444
|
+
const selectedHosts = resolveInstallHosts(options.host);
|
|
445
|
+
const uninstallers = {
|
|
446
|
+
codex: () => uninstallCodexHost(options),
|
|
447
|
+
claude: () => uninstallClaudeHost(options),
|
|
448
|
+
gemini: () => uninstallGeminiHost(options),
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const uninstalledHosts = selectedHosts.map((host) => uninstallers[host]());
|
|
452
|
+
return {
|
|
453
|
+
uninstalledHosts,
|
|
454
|
+
};
|
|
455
|
+
}
|