helloloop 0.2.1 → 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/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 +90 -23
- 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 +153 -0
- 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
|
@@ -52,6 +52,14 @@ function removeTargetIfNeeded(targetPath, force) {
|
|
|
52
52
|
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
53
53
|
}
|
|
54
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
|
+
|
|
55
63
|
function copyBundleEntries(bundleRoot, targetRoot, entries) {
|
|
56
64
|
for (const entry of entries) {
|
|
57
65
|
const sourcePath = path.join(bundleRoot, entry);
|
|
@@ -112,6 +120,23 @@ function updateCodexMarketplace(marketplaceFile) {
|
|
|
112
120
|
writeJson(marketplaceFile, marketplace);
|
|
113
121
|
}
|
|
114
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
|
+
|
|
115
140
|
function updateClaudeSettings(settingsFile, marketplaceRoot) {
|
|
116
141
|
const settings = fileExists(settingsFile)
|
|
117
142
|
? readJson(settingsFile)
|
|
@@ -131,6 +156,30 @@ function updateClaudeSettings(settingsFile, marketplaceRoot) {
|
|
|
131
156
|
writeJson(settingsFile, settings);
|
|
132
157
|
}
|
|
133
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
|
+
|
|
134
183
|
function updateClaudeKnownMarketplaces(knownMarketplacesFile, marketplaceRoot, updatedAt) {
|
|
135
184
|
const knownMarketplaces = fileExists(knownMarketplacesFile)
|
|
136
185
|
? readJson(knownMarketplacesFile)
|
|
@@ -148,6 +197,21 @@ function updateClaudeKnownMarketplaces(knownMarketplacesFile, marketplaceRoot, u
|
|
|
148
197
|
writeJson(knownMarketplacesFile, knownMarketplaces);
|
|
149
198
|
}
|
|
150
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
|
+
|
|
151
215
|
function updateClaudeInstalledPlugins(installedPluginsFile, pluginRoot, pluginVersion, updatedAt) {
|
|
152
216
|
const installedPlugins = fileExists(installedPluginsFile)
|
|
153
217
|
? readJson(installedPluginsFile)
|
|
@@ -171,6 +235,21 @@ function updateClaudeInstalledPlugins(installedPluginsFile, pluginRoot, pluginVe
|
|
|
171
235
|
writeJson(installedPluginsFile, installedPlugins);
|
|
172
236
|
}
|
|
173
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
|
+
|
|
174
253
|
function installCodexHost(bundleRoot, options) {
|
|
175
254
|
const resolvedCodexHome = resolveHomeDir(options.codexHome, ".codex");
|
|
176
255
|
const targetPluginsRoot = path.join(resolvedCodexHome, "plugins");
|
|
@@ -205,6 +284,23 @@ function installCodexHost(bundleRoot, options) {
|
|
|
205
284
|
};
|
|
206
285
|
}
|
|
207
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
|
+
|
|
208
304
|
function installClaudeHost(bundleRoot, options) {
|
|
209
305
|
const resolvedClaudeHome = resolveHomeDir(options.claudeHome, ".claude");
|
|
210
306
|
const sourceMarketplaceRoot = path.join(bundleRoot, "hosts", "claude", "marketplace");
|
|
@@ -249,6 +345,37 @@ function installClaudeHost(bundleRoot, options) {
|
|
|
249
345
|
};
|
|
250
346
|
}
|
|
251
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
|
+
|
|
252
379
|
function installGeminiHost(bundleRoot, options) {
|
|
253
380
|
const resolvedGeminiHome = resolveHomeDir(options.geminiHome, ".gemini");
|
|
254
381
|
const sourceExtensionRoot = path.join(bundleRoot, "hosts", "gemini", "extension");
|
|
@@ -271,6 +398,18 @@ function installGeminiHost(bundleRoot, options) {
|
|
|
271
398
|
};
|
|
272
399
|
}
|
|
273
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
|
+
|
|
274
413
|
function resolveInstallHosts(hostOption) {
|
|
275
414
|
const normalized = String(hostOption || "codex").trim().toLowerCase();
|
|
276
415
|
if (normalized === "all") {
|
|
@@ -300,3 +439,17 @@ export function installPluginBundle(options = {}) {
|
|
|
300
439
|
marketplaceFile: codexResult?.marketplaceFile || "",
|
|
301
440
|
};
|
|
302
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
|
+
}
|
package/src/rebuild.mjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { ensureDir, nowIso, writeJson } from "./common.mjs";
|
|
5
|
+
|
|
6
|
+
const PROTECTED_TOP_LEVEL = new Set([
|
|
7
|
+
".git",
|
|
8
|
+
".gitignore",
|
|
9
|
+
".gitattributes",
|
|
10
|
+
".helloagents",
|
|
11
|
+
".helloloop",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function ensureSafeRepoRoot(repoRoot) {
|
|
15
|
+
const absoluteRepoRoot = path.resolve(repoRoot);
|
|
16
|
+
const parsed = path.parse(absoluteRepoRoot);
|
|
17
|
+
if (absoluteRepoRoot === parsed.root) {
|
|
18
|
+
throw new Error(`拒绝清理根目录:${absoluteRepoRoot}`);
|
|
19
|
+
}
|
|
20
|
+
if (!fs.existsSync(absoluteRepoRoot) || !fs.statSync(absoluteRepoRoot).isDirectory()) {
|
|
21
|
+
throw new Error(`项目目录不存在或不是目录:${absoluteRepoRoot}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function collectDocsInsideRepo(repoRoot, discovery) {
|
|
26
|
+
const resolvedDocs = Array.isArray(discovery?.resolvedDocs) ? discovery.resolvedDocs : [];
|
|
27
|
+
return resolvedDocs
|
|
28
|
+
.map((item) => ({
|
|
29
|
+
absolutePath: path.resolve(item.absolutePath),
|
|
30
|
+
relativePath: String(item.relativePath || "").replaceAll("\\", "/"),
|
|
31
|
+
}))
|
|
32
|
+
.filter((item) => {
|
|
33
|
+
const relative = path.relative(repoRoot, item.absolutePath);
|
|
34
|
+
return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stagePreservedDocs(configRoot, docs) {
|
|
39
|
+
const stageRoot = path.join(configRoot, "rebuild-staging", nowIso().replaceAll(":", "-").replaceAll(".", "-"));
|
|
40
|
+
for (const doc of docs) {
|
|
41
|
+
const stagedTarget = path.join(stageRoot, doc.relativePath);
|
|
42
|
+
ensureDir(path.dirname(stagedTarget));
|
|
43
|
+
fs.copyFileSync(doc.absolutePath, stagedTarget);
|
|
44
|
+
}
|
|
45
|
+
return stageRoot;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function removeUnprotectedTopLevel(repoRoot) {
|
|
49
|
+
const removedEntries = [];
|
|
50
|
+
for (const entry of fs.readdirSync(repoRoot, { withFileTypes: true })) {
|
|
51
|
+
if (PROTECTED_TOP_LEVEL.has(entry.name)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const entryPath = path.join(repoRoot, entry.name);
|
|
56
|
+
fs.rmSync(entryPath, { recursive: true, force: true });
|
|
57
|
+
removedEntries.push(entry.name);
|
|
58
|
+
}
|
|
59
|
+
return removedEntries;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function restoreDocsFromStage(repoRoot, stageRoot, docs) {
|
|
63
|
+
for (const doc of docs) {
|
|
64
|
+
const stagedSource = path.join(stageRoot, doc.relativePath);
|
|
65
|
+
const targetPath = path.join(repoRoot, doc.relativePath);
|
|
66
|
+
ensureDir(path.dirname(targetPath));
|
|
67
|
+
fs.copyFileSync(stagedSource, targetPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resetLoopRuntime(context) {
|
|
72
|
+
for (const target of [
|
|
73
|
+
context.backlogFile,
|
|
74
|
+
context.projectFile,
|
|
75
|
+
context.statusFile,
|
|
76
|
+
context.stateFile,
|
|
77
|
+
]) {
|
|
78
|
+
if (fs.existsSync(target)) {
|
|
79
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (fs.existsSync(context.runsDir)) {
|
|
84
|
+
fs.rmSync(context.runsDir, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function resetRepoForRebuild(context, discovery) {
|
|
89
|
+
ensureSafeRepoRoot(context.repoRoot);
|
|
90
|
+
|
|
91
|
+
const preservedDocs = collectDocsInsideRepo(context.repoRoot, discovery);
|
|
92
|
+
const stageRoot = preservedDocs.length
|
|
93
|
+
? stagePreservedDocs(context.configRoot, preservedDocs)
|
|
94
|
+
: "";
|
|
95
|
+
|
|
96
|
+
const manifestFile = path.join(context.configRoot, "rebuild-manifest.json");
|
|
97
|
+
const removedEntries = removeUnprotectedTopLevel(context.repoRoot);
|
|
98
|
+
resetLoopRuntime(context);
|
|
99
|
+
|
|
100
|
+
if (stageRoot) {
|
|
101
|
+
restoreDocsFromStage(context.repoRoot, stageRoot, preservedDocs);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
writeJson(manifestFile, {
|
|
105
|
+
updatedAt: nowIso(),
|
|
106
|
+
repoRoot: context.repoRoot,
|
|
107
|
+
removedEntries,
|
|
108
|
+
preservedDocs: preservedDocs.map((item) => item.relativePath),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
removedEntries,
|
|
113
|
+
preservedDocs: preservedDocs.map((item) => item.relativePath),
|
|
114
|
+
manifestFile,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -50,6 +50,64 @@
|
|
|
50
50
|
"type": "string"
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
|
+
"requestInterpretation": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"additionalProperties": false,
|
|
56
|
+
"required": [
|
|
57
|
+
"summary",
|
|
58
|
+
"priorities",
|
|
59
|
+
"cautions"
|
|
60
|
+
],
|
|
61
|
+
"properties": {
|
|
62
|
+
"summary": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"minLength": 1
|
|
65
|
+
},
|
|
66
|
+
"priorities": {
|
|
67
|
+
"type": "array",
|
|
68
|
+
"items": {
|
|
69
|
+
"type": "string"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"cautions": {
|
|
73
|
+
"type": "array",
|
|
74
|
+
"items": {
|
|
75
|
+
"type": "string"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"repoDecision": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"additionalProperties": false,
|
|
83
|
+
"required": [
|
|
84
|
+
"compatibility",
|
|
85
|
+
"action",
|
|
86
|
+
"reason"
|
|
87
|
+
],
|
|
88
|
+
"properties": {
|
|
89
|
+
"compatibility": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"enum": [
|
|
92
|
+
"compatible",
|
|
93
|
+
"conflict",
|
|
94
|
+
"uncertain"
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
"action": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"enum": [
|
|
100
|
+
"continue_existing",
|
|
101
|
+
"confirm_rebuild",
|
|
102
|
+
"start_new"
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
"reason": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"minLength": 1
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
53
111
|
"tasks": {
|
|
54
112
|
"type": "array",
|
|
55
113
|
"minItems": 1,
|