helloloop 0.1.1 → 0.2.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 +14 -0
- package/.codex-plugin/plugin.json +6 -6
- package/LICENSE +176 -0
- package/README.md +323 -146
- package/hosts/claude/marketplace/.claude-plugin/marketplace.json +14 -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 +9 -3
- package/skills/helloloop/SKILL.md +59 -31
- package/src/analyze_confirmation.mjs +128 -0
- package/src/analyze_prompt.mjs +75 -0
- package/src/analyzer.mjs +208 -0
- package/src/cli.mjs +148 -91
- package/src/cli_support.mjs +307 -0
- package/src/discovery.mjs +155 -0
- package/src/discovery_inference.mjs +220 -0
- package/src/discovery_paths.mjs +166 -0
- package/src/guardrails.mjs +59 -0
- package/src/install.mjs +142 -28
- package/src/process.mjs +31 -129
- package/src/prompt.mjs +10 -12
- package/src/shell_invocation.mjs +225 -0
- package/templates/analysis-output.schema.json +144 -0
package/src/cli.mjs
CHANGED
|
@@ -1,16 +1,44 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
2
|
|
|
3
|
+
import { analyzeExecution } from "./backlog.mjs";
|
|
4
|
+
import { renderAnalyzeConfirmation, resolveAutoRunMaxTasks } from "./analyze_confirmation.mjs";
|
|
5
|
+
import {
|
|
6
|
+
confirmAutoExecution,
|
|
7
|
+
renderAnalyzeStopMessage,
|
|
8
|
+
renderAutoRunSummary,
|
|
9
|
+
runDoctor,
|
|
10
|
+
} from "./cli_support.mjs";
|
|
4
11
|
import { createContext } from "./context.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import { scaffoldIfMissing } from "./config.mjs";
|
|
12
|
+
import { analyzeWorkspace } from "./analyzer.mjs";
|
|
13
|
+
import { loadBacklog, scaffoldIfMissing } from "./config.mjs";
|
|
14
|
+
import { resolveRepoRoot } from "./discovery.mjs";
|
|
7
15
|
import { installPluginBundle } from "./install.mjs";
|
|
8
16
|
import { runLoop, runOnce, renderStatusText } from "./runner.mjs";
|
|
9
17
|
|
|
10
18
|
const REPO_ROOT_PLACEHOLDER = "<REPO_ROOT>";
|
|
19
|
+
const DOCS_PATH_PLACEHOLDER = "<DOCS_PATH>";
|
|
20
|
+
const KNOWN_COMMANDS = new Set([
|
|
21
|
+
"analyze",
|
|
22
|
+
"install",
|
|
23
|
+
"init",
|
|
24
|
+
"status",
|
|
25
|
+
"next",
|
|
26
|
+
"run-once",
|
|
27
|
+
"run-loop",
|
|
28
|
+
"doctor",
|
|
29
|
+
"help",
|
|
30
|
+
"--help",
|
|
31
|
+
"-h",
|
|
32
|
+
]);
|
|
11
33
|
|
|
12
34
|
function parseArgs(argv) {
|
|
13
|
-
const [
|
|
35
|
+
const [first = "", ...restArgs] = argv;
|
|
36
|
+
const command = !first
|
|
37
|
+
? "analyze"
|
|
38
|
+
: (KNOWN_COMMANDS.has(first) ? first : "analyze");
|
|
39
|
+
const rest = !first
|
|
40
|
+
? []
|
|
41
|
+
: (KNOWN_COMMANDS.has(first) ? restArgs : argv);
|
|
14
42
|
const options = {
|
|
15
43
|
requiredDocs: [],
|
|
16
44
|
constraints: [],
|
|
@@ -19,6 +47,7 @@ function parseArgs(argv) {
|
|
|
19
47
|
for (let index = 0; index < rest.length; index += 1) {
|
|
20
48
|
const arg = rest[index];
|
|
21
49
|
if (arg === "--dry-run") options.dryRun = true;
|
|
50
|
+
else if (arg === "--yes" || arg === "-y") options.yes = true;
|
|
22
51
|
else if (arg === "--allow-high-risk") options.allowHighRisk = true;
|
|
23
52
|
else if (arg === "--force") options.force = true;
|
|
24
53
|
else if (arg === "--task-id") { options.taskId = rest[index + 1]; index += 1; }
|
|
@@ -26,10 +55,15 @@ function parseArgs(argv) {
|
|
|
26
55
|
else if (arg === "--max-attempts") { options.maxAttempts = Number(rest[index + 1]); index += 1; }
|
|
27
56
|
else if (arg === "--max-strategies") { options.maxStrategies = Number(rest[index + 1]); index += 1; }
|
|
28
57
|
else if (arg === "--repo") { options.repoRoot = rest[index + 1]; index += 1; }
|
|
58
|
+
else if (arg === "--docs") { options.docsPath = rest[index + 1]; index += 1; }
|
|
59
|
+
else if (arg === "--host") { options.host = rest[index + 1]; index += 1; }
|
|
29
60
|
else if (arg === "--codex-home") { options.codexHome = rest[index + 1]; index += 1; }
|
|
61
|
+
else if (arg === "--claude-home") { options.claudeHome = rest[index + 1]; index += 1; }
|
|
62
|
+
else if (arg === "--gemini-home") { options.geminiHome = rest[index + 1]; index += 1; }
|
|
30
63
|
else if (arg === "--config-dir") { options.configDirName = rest[index + 1]; index += 1; }
|
|
31
64
|
else if (arg === "--required-doc") { options.requiredDocs.push(rest[index + 1]); index += 1; }
|
|
32
65
|
else if (arg === "--constraint") { options.constraints.push(rest[index + 1]); index += 1; }
|
|
66
|
+
else if (!options.inputPath) { options.inputPath = arg; }
|
|
33
67
|
else {
|
|
34
68
|
throw new Error(`未知参数:${arg}`);
|
|
35
69
|
}
|
|
@@ -40,9 +74,10 @@ function parseArgs(argv) {
|
|
|
40
74
|
|
|
41
75
|
function helpText() {
|
|
42
76
|
return [
|
|
43
|
-
"用法:helloloop
|
|
77
|
+
"用法:helloloop [command] [path] [options]",
|
|
44
78
|
"",
|
|
45
79
|
"命令:",
|
|
80
|
+
" analyze 自动分析并生成执行确认单;确认后继续自动接续开发(默认)",
|
|
46
81
|
" install 安装插件到 Codex Home(适合 npx / npm bin 分发)",
|
|
47
82
|
" init 初始化 .helloloop 配置",
|
|
48
83
|
" status 查看 backlog 与下一任务",
|
|
@@ -52,10 +87,15 @@ function helpText() {
|
|
|
52
87
|
" doctor 检查 Codex、当前插件 bundle 与目标仓库 .helloloop 配置是否可用",
|
|
53
88
|
"",
|
|
54
89
|
"选项:",
|
|
90
|
+
" --host <name> 安装宿主:codex | claude | gemini | all(默认 codex)",
|
|
55
91
|
" --codex-home <dir> Codex Home,install 默认使用 ~/.codex",
|
|
56
|
-
" --
|
|
92
|
+
" --claude-home <dir> Claude Home,install 默认使用 ~/.claude",
|
|
93
|
+
" --gemini-home <dir> Gemini Home,install 默认使用 ~/.gemini",
|
|
94
|
+
" --repo <dir> 高级选项:显式指定项目仓库根目录",
|
|
95
|
+
" --docs <dir|file> 高级选项:显式指定开发文档目录或文件",
|
|
57
96
|
" --config-dir <dir> 配置目录,默认 .helloloop",
|
|
58
|
-
" --
|
|
97
|
+
" -y, --yes 跳过交互确认,分析后直接开始自动执行",
|
|
98
|
+
" --dry-run 只分析并输出确认单,不真正开始自动执行",
|
|
59
99
|
" --task-id <id> 指定任务 id",
|
|
60
100
|
" --max-tasks <n> run-loop 最多执行 n 个任务",
|
|
61
101
|
" --max-attempts <n> 每种策略内最多重试 n 次",
|
|
@@ -73,90 +113,55 @@ function printHelp() {
|
|
|
73
113
|
function renderFollowupExamples() {
|
|
74
114
|
return [
|
|
75
115
|
"下一步示例:",
|
|
76
|
-
`npx helloloop
|
|
77
|
-
|
|
116
|
+
`npx helloloop`,
|
|
117
|
+
`npx helloloop <PATH>`,
|
|
118
|
+
`npx helloloop --dry-run`,
|
|
119
|
+
`npx helloloop install --host all`,
|
|
120
|
+
`npx helloloop next`,
|
|
121
|
+
`如需显式补充路径:npx helloloop --repo ${REPO_ROOT_PLACEHOLDER} --docs ${DOCS_PATH_PLACEHOLDER}`,
|
|
78
122
|
].join("\n");
|
|
79
123
|
}
|
|
80
124
|
|
|
81
|
-
function
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
encoding: "utf8",
|
|
85
|
-
shell: true,
|
|
86
|
-
})
|
|
87
|
-
: spawnSync("codex", ["--version"], {
|
|
88
|
-
encoding: "utf8",
|
|
89
|
-
shell: false,
|
|
90
|
-
});
|
|
91
|
-
const ok = codexVersion.status === 0;
|
|
92
|
-
return {
|
|
93
|
-
ok,
|
|
94
|
-
detail: ok
|
|
95
|
-
? String(codexVersion.stdout || "").trim()
|
|
96
|
-
: String(codexVersion.stderr || codexVersion.error || "无法执行 codex --version").trim(),
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function collectDoctorChecks(context) {
|
|
101
|
-
const codexVersion = probeCodexVersion();
|
|
102
|
-
return [
|
|
103
|
-
{
|
|
104
|
-
name: "codex CLI",
|
|
105
|
-
ok: codexVersion.ok,
|
|
106
|
-
detail: codexVersion.detail,
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
name: "backlog.json",
|
|
110
|
-
ok: fileExists(context.backlogFile),
|
|
111
|
-
detail: context.backlogFile,
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
name: "policy.json",
|
|
115
|
-
ok: fileExists(context.policyFile),
|
|
116
|
-
detail: context.policyFile,
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
name: "verify.yaml",
|
|
120
|
-
ok: fileExists(context.repoVerifyFile),
|
|
121
|
-
detail: context.repoVerifyFile,
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
name: "project.json",
|
|
125
|
-
ok: fileExists(context.projectFile),
|
|
126
|
-
detail: context.projectFile,
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
name: "plugin manifest",
|
|
130
|
-
ok: fileExists(context.pluginManifestFile),
|
|
131
|
-
detail: context.pluginManifestFile,
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
name: "plugin skill",
|
|
135
|
-
ok: fileExists(context.skillFile),
|
|
136
|
-
detail: context.skillFile,
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
name: "install script",
|
|
140
|
-
ok: fileExists(context.installScriptFile),
|
|
141
|
-
detail: context.installScriptFile,
|
|
142
|
-
},
|
|
125
|
+
function renderInstallSummary(result) {
|
|
126
|
+
const lines = [
|
|
127
|
+
"HelloLoop 已安装到以下宿主:",
|
|
143
128
|
];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function runDoctor(context) {
|
|
147
|
-
const checks = collectDoctorChecks(context);
|
|
148
129
|
|
|
149
|
-
for (const item of
|
|
150
|
-
|
|
130
|
+
for (const item of result.installedHosts) {
|
|
131
|
+
lines.push(`- ${item.displayName}:${item.targetRoot}`);
|
|
132
|
+
if (item.marketplaceFile) {
|
|
133
|
+
lines.push(` marketplace:${item.marketplaceFile}`);
|
|
134
|
+
}
|
|
135
|
+
if (item.settingsFile) {
|
|
136
|
+
lines.push(` settings:${item.settingsFile}`);
|
|
137
|
+
}
|
|
151
138
|
}
|
|
152
139
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
140
|
+
lines.push("");
|
|
141
|
+
lines.push("使用入口:");
|
|
142
|
+
lines.push("- Codex:`$helloloop` / `npx helloloop`");
|
|
143
|
+
lines.push("- Claude:`/helloloop`");
|
|
144
|
+
lines.push("- Gemini:`/helloloop`");
|
|
145
|
+
lines.push("");
|
|
146
|
+
lines.push(renderFollowupExamples());
|
|
147
|
+
return lines.join("\n");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolveContextFromOptions(options) {
|
|
151
|
+
const resolvedRepo = resolveRepoRoot({
|
|
152
|
+
cwd: process.cwd(),
|
|
153
|
+
repoRoot: options.repoRoot,
|
|
154
|
+
inputPath: options.inputPath,
|
|
155
|
+
});
|
|
156
156
|
|
|
157
|
-
if (
|
|
158
|
-
|
|
157
|
+
if (!resolvedRepo.ok) {
|
|
158
|
+
throw new Error(resolvedRepo.message);
|
|
159
159
|
}
|
|
160
|
+
|
|
161
|
+
return createContext({
|
|
162
|
+
repoRoot: resolvedRepo.repoRoot,
|
|
163
|
+
configDirName: options.configDirName,
|
|
164
|
+
});
|
|
160
165
|
}
|
|
161
166
|
|
|
162
167
|
export async function runCli(argv) {
|
|
@@ -167,24 +172,76 @@ export async function runCli(argv) {
|
|
|
167
172
|
return;
|
|
168
173
|
}
|
|
169
174
|
|
|
170
|
-
const context = createContext({
|
|
171
|
-
repoRoot: options.repoRoot,
|
|
172
|
-
configDirName: options.configDirName,
|
|
173
|
-
});
|
|
174
|
-
|
|
175
175
|
if (command === "install") {
|
|
176
|
+
const context = createContext({
|
|
177
|
+
repoRoot: options.repoRoot,
|
|
178
|
+
configDirName: options.configDirName,
|
|
179
|
+
});
|
|
176
180
|
const result = installPluginBundle({
|
|
177
181
|
bundleRoot: context.bundleRoot,
|
|
182
|
+
host: options.host,
|
|
178
183
|
codexHome: options.codexHome,
|
|
184
|
+
claudeHome: options.claudeHome,
|
|
185
|
+
geminiHome: options.geminiHome,
|
|
179
186
|
force: options.force,
|
|
180
187
|
});
|
|
181
|
-
console.log(
|
|
182
|
-
|
|
188
|
+
console.log(renderInstallSummary(result));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (command === "analyze") {
|
|
193
|
+
const result = await analyzeWorkspace({
|
|
194
|
+
cwd: process.cwd(),
|
|
195
|
+
inputPath: options.inputPath,
|
|
196
|
+
repoRoot: options.repoRoot,
|
|
197
|
+
docsPath: options.docsPath,
|
|
198
|
+
configDirName: options.configDirName,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (!result.ok) {
|
|
202
|
+
console.error(result.summary);
|
|
203
|
+
process.exitCode = 1;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const confirmationText = renderAnalyzeConfirmation(result.context, result.analysis, result.backlog, options);
|
|
208
|
+
const execution = analyzeExecution(result.backlog, options);
|
|
209
|
+
|
|
210
|
+
console.log(confirmationText);
|
|
211
|
+
console.log("");
|
|
212
|
+
|
|
213
|
+
if (options.dryRun) {
|
|
214
|
+
console.log("已按 --dry-run 跳过自动执行。");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (execution.state !== "ready") {
|
|
219
|
+
console.log(renderAnalyzeStopMessage(execution.blockedReason || "当前 backlog 已无可自动执行任务。"));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const approved = options.yes ? true : await confirmAutoExecution();
|
|
224
|
+
if (!approved) {
|
|
225
|
+
console.log("已取消自动执行;分析结果与 backlog 已保留在 .helloloop/。");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
183
229
|
console.log("");
|
|
184
|
-
console.log(
|
|
230
|
+
console.log("开始自动接续执行...");
|
|
231
|
+
const results = await runLoop(result.context, {
|
|
232
|
+
...options,
|
|
233
|
+
maxTasks: resolveAutoRunMaxTasks(result.backlog, options),
|
|
234
|
+
});
|
|
235
|
+
const refreshedBacklog = loadBacklog(result.context);
|
|
236
|
+
console.log(renderAutoRunSummary(result.context, refreshedBacklog, results, options));
|
|
237
|
+
if (results.some((item) => !item.ok)) {
|
|
238
|
+
process.exitCode = 1;
|
|
239
|
+
}
|
|
185
240
|
return;
|
|
186
241
|
}
|
|
187
242
|
|
|
243
|
+
const context = resolveContextFromOptions(options);
|
|
244
|
+
|
|
188
245
|
if (command === "init") {
|
|
189
246
|
const created = scaffoldIfMissing(context);
|
|
190
247
|
if (!created.length) {
|
|
@@ -199,7 +256,7 @@ export async function runCli(argv) {
|
|
|
199
256
|
}
|
|
200
257
|
|
|
201
258
|
if (command === "doctor") {
|
|
202
|
-
await runDoctor(context);
|
|
259
|
+
await runDoctor(context, options);
|
|
203
260
|
return;
|
|
204
261
|
}
|
|
205
262
|
|
|
@@ -0,0 +1,307 @@
|
|
|
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 settings = fileExists(settingsFile) ? readJson(settingsFile) : {};
|
|
155
|
+
checks.push({
|
|
156
|
+
name: "claude installed marketplace",
|
|
157
|
+
ok: fileExists(path.join(options.claudeHome, "marketplaces", "helloloop-local", ".claude-plugin", "marketplace.json")),
|
|
158
|
+
detail: path.join(options.claudeHome, "marketplaces", "helloloop-local", ".claude-plugin", "marketplace.json"),
|
|
159
|
+
});
|
|
160
|
+
checks.push({
|
|
161
|
+
name: "claude installed plugin",
|
|
162
|
+
ok: fileExists(path.join(options.claudeHome, "marketplaces", "helloloop-local", "plugins", "helloloop", ".claude-plugin", "plugin.json")),
|
|
163
|
+
detail: path.join(options.claudeHome, "marketplaces", "helloloop-local", "plugins", "helloloop", ".claude-plugin", "plugin.json"),
|
|
164
|
+
});
|
|
165
|
+
checks.push({
|
|
166
|
+
name: "claude settings enabled",
|
|
167
|
+
ok: settings?.enabledPlugins?.["helloloop@helloloop-local"] === true,
|
|
168
|
+
detail: settingsFile,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return checks;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function collectGeminiDoctorChecks(context, options = {}) {
|
|
176
|
+
const geminiVersion = probeNamedCliVersion("gemini", "Gemini");
|
|
177
|
+
const checks = [
|
|
178
|
+
{
|
|
179
|
+
name: "gemini CLI",
|
|
180
|
+
ok: geminiVersion.ok,
|
|
181
|
+
detail: geminiVersion.detail,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: "gemini extension manifest",
|
|
185
|
+
ok: fileExists(path.join(context.bundleRoot, "hosts", "gemini", "extension", "gemini-extension.json")),
|
|
186
|
+
detail: path.join(context.bundleRoot, "hosts", "gemini", "extension", "gemini-extension.json"),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: "gemini command",
|
|
190
|
+
ok: fileExists(path.join(context.bundleRoot, "hosts", "gemini", "extension", "commands", "helloloop.toml")),
|
|
191
|
+
detail: path.join(context.bundleRoot, "hosts", "gemini", "extension", "commands", "helloloop.toml"),
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "gemini context file",
|
|
195
|
+
ok: fileExists(path.join(context.bundleRoot, "hosts", "gemini", "extension", "GEMINI.md")),
|
|
196
|
+
detail: path.join(context.bundleRoot, "hosts", "gemini", "extension", "GEMINI.md"),
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
if (options.geminiHome) {
|
|
201
|
+
checks.push({
|
|
202
|
+
name: "gemini installed extension",
|
|
203
|
+
ok: fileExists(path.join(options.geminiHome, "extensions", "helloloop", "gemini-extension.json")),
|
|
204
|
+
detail: path.join(options.geminiHome, "extensions", "helloloop", "gemini-extension.json"),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return checks;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function runDoctor(context, options = {}) {
|
|
212
|
+
const hosts = normalizeDoctorHosts(options.host);
|
|
213
|
+
const checks = hosts.flatMap((host) => {
|
|
214
|
+
if (host === "codex") return collectCodexDoctorChecks(context, options);
|
|
215
|
+
if (host === "claude") return collectClaudeDoctorChecks(context, options);
|
|
216
|
+
if (host === "gemini") return collectGeminiDoctorChecks(context, options);
|
|
217
|
+
return [];
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
for (const item of checks) {
|
|
221
|
+
console.log(`${item.ok ? "OK" : "FAIL"} ${item.name} ${item.detail}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (checks.every((item) => item.ok)) {
|
|
225
|
+
console.log("\nDoctor 结论:当前 HelloLoop 所选宿主与目标仓库已具备基本运行条件。");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (checks.some((item) => !item.ok)) {
|
|
229
|
+
process.exitCode = 1;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isAffirmativeAnswer(answer) {
|
|
234
|
+
const raw = String(answer || "").trim();
|
|
235
|
+
const normalized = raw.toLowerCase();
|
|
236
|
+
return [
|
|
237
|
+
"y",
|
|
238
|
+
"yes",
|
|
239
|
+
"ok",
|
|
240
|
+
"确认",
|
|
241
|
+
"是",
|
|
242
|
+
"继续",
|
|
243
|
+
"好的",
|
|
244
|
+
].includes(normalized) || ["确认", "是", "继续", "好的"].includes(raw);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function confirmAutoExecution() {
|
|
248
|
+
const readline = createInterface({
|
|
249
|
+
input: process.stdin,
|
|
250
|
+
output: process.stdout,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const answer = await readline.question("是否开始自动接续执行?输入 y / yes / 确认 继续,其它任意输入取消:");
|
|
255
|
+
return isAffirmativeAnswer(answer);
|
|
256
|
+
} finally {
|
|
257
|
+
readline.close();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function renderAnalyzeStopMessage(reason) {
|
|
262
|
+
return reason || "当前没有可自动执行的任务。";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function renderAutoRunSummary(context, backlog, results, options = {}) {
|
|
266
|
+
const summary = summarizeBacklog(backlog);
|
|
267
|
+
const execution = analyzeExecution(backlog, options);
|
|
268
|
+
const lines = [
|
|
269
|
+
"自动执行结果",
|
|
270
|
+
"============",
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
if (!results.length) {
|
|
274
|
+
lines.push("- 本轮未执行任何任务");
|
|
275
|
+
} else {
|
|
276
|
+
for (const item of results) {
|
|
277
|
+
if (!item.task) {
|
|
278
|
+
lines.push(`- 已停止:${item.summary || item.kind || "没有更多任务"}`);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
lines.push(`- ${item.ok ? "完成任务" : "失败任务"}:${item.task.title}`);
|
|
282
|
+
if (!item.ok && item.summary) {
|
|
283
|
+
lines.push(item.summary);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
lines.push("");
|
|
289
|
+
lines.push("当前统计:");
|
|
290
|
+
lines.push(`- 已完成:${summary.done}`);
|
|
291
|
+
lines.push(`- 待处理:${summary.pending}`);
|
|
292
|
+
lines.push(`- 进行中:${summary.inProgress}`);
|
|
293
|
+
lines.push(`- 阻塞:${summary.blocked}`);
|
|
294
|
+
lines.push(`- 失败:${summary.failed}`);
|
|
295
|
+
lines.push(`- 状态文件:${context.statusFile.replaceAll("\\", "/")}`);
|
|
296
|
+
lines.push("");
|
|
297
|
+
lines.push("结论:");
|
|
298
|
+
if (execution.state === "done") {
|
|
299
|
+
lines.push("- backlog 已全部完成");
|
|
300
|
+
} else if (execution.blockedReason) {
|
|
301
|
+
lines.push(`- 当前停止原因:${execution.blockedReason}`);
|
|
302
|
+
} else {
|
|
303
|
+
lines.push("- 当前轮次已结束,可继续查看 status 或下一任务");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return lines.join("\n");
|
|
307
|
+
}
|