helloloop 0.3.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/README.md +194 -81
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +17 -13
- package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +12 -5
- package/hosts/gemini/extension/GEMINI.md +14 -7
- package/hosts/gemini/extension/commands/helloloop.toml +17 -12
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/skills/helloloop/SKILL.md +18 -7
- package/src/analyze_confirmation.mjs +29 -5
- package/src/analyze_prompt.mjs +5 -1
- package/src/analyze_user_input.mjs +20 -2
- package/src/analyzer.mjs +130 -43
- package/src/cli.mjs +32 -492
- package/src/cli_analyze_command.mjs +248 -0
- package/src/cli_args.mjs +106 -0
- package/src/cli_command_handlers.mjs +120 -0
- package/src/cli_context.mjs +31 -0
- package/src/cli_render.mjs +70 -0
- package/src/cli_support.mjs +11 -14
- package/src/completion_review.mjs +243 -0
- package/src/config.mjs +51 -0
- package/src/discovery.mjs +21 -2
- package/src/discovery_prompt.mjs +2 -27
- package/src/email_notification.mjs +343 -0
- package/src/engine_metadata.mjs +79 -0
- package/src/engine_process_support.mjs +294 -0
- package/src/engine_selection.mjs +335 -0
- package/src/engine_selection_failure.mjs +51 -0
- package/src/engine_selection_messages.mjs +119 -0
- package/src/engine_selection_probe.mjs +78 -0
- package/src/engine_selection_prompt.mjs +48 -0
- package/src/engine_selection_settings.mjs +104 -0
- package/src/global_config.mjs +21 -0
- package/src/guardrails.mjs +15 -4
- package/src/install.mjs +6 -405
- package/src/install_claude.mjs +189 -0
- package/src/install_codex.mjs +114 -0
- package/src/install_gemini.mjs +43 -0
- package/src/install_shared.mjs +138 -0
- package/src/process.mjs +567 -100
- package/src/prompt.mjs +9 -5
- package/src/prompt_session.mjs +40 -0
- package/src/runner.mjs +3 -341
- package/src/runner_execute_task.mjs +255 -0
- package/src/runner_execution_support.mjs +146 -0
- package/src/runner_loop.mjs +106 -0
- package/src/runner_once.mjs +29 -0
- package/src/runner_status.mjs +104 -0
- package/src/runtime_recovery.mjs +302 -0
- package/src/shell_invocation.mjs +16 -0
- package/templates/analysis-output.schema.json +0 -1
- package/templates/policy.template.json +25 -0
- package/templates/project.template.json +2 -0
- package/templates/task-review-output.schema.json +70 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { analyzeExecution } from "./backlog.mjs";
|
|
2
|
+
import { renderAnalyzeConfirmation, resolveAutoRunMaxTasks } from "./analyze_confirmation.mjs";
|
|
3
|
+
import {
|
|
4
|
+
confirmAutoExecution,
|
|
5
|
+
confirmRepoConflictResolution,
|
|
6
|
+
renderAnalyzeStopMessage,
|
|
7
|
+
renderAutoRunSummary,
|
|
8
|
+
renderRepoConflictStopMessage,
|
|
9
|
+
} from "./cli_support.mjs";
|
|
10
|
+
import { analyzeWorkspace } from "./analyzer.mjs";
|
|
11
|
+
import {
|
|
12
|
+
hasBlockingInputIssues,
|
|
13
|
+
renderBlockingInputIssueMessage,
|
|
14
|
+
} from "./analyze_user_input.mjs";
|
|
15
|
+
import { loadBacklog, loadPolicy } from "./config.mjs";
|
|
16
|
+
import { createContext } from "./context.mjs";
|
|
17
|
+
import { createDiscoveryPromptSession, resolveDiscoveryFailureInteractively } from "./discovery_prompt.mjs";
|
|
18
|
+
import { resolveEngineSelection } from "./engine_selection.mjs";
|
|
19
|
+
import { resetRepoForRebuild } from "./rebuild.mjs";
|
|
20
|
+
import { runLoop } from "./runner.mjs";
|
|
21
|
+
import { renderRebuildSummary } from "./cli_render.mjs";
|
|
22
|
+
import { shouldConfirmRepoRebuild } from "./cli_support.mjs";
|
|
23
|
+
|
|
24
|
+
async function resolveAnalyzeEngineSelection(options) {
|
|
25
|
+
if (options.engineResolution?.ok) {
|
|
26
|
+
return options.engineResolution;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const provisionalContext = createContext({
|
|
30
|
+
repoRoot: options.repoRoot || process.cwd(),
|
|
31
|
+
configDirName: options.configDirName,
|
|
32
|
+
});
|
|
33
|
+
return resolveEngineSelection({
|
|
34
|
+
context: provisionalContext,
|
|
35
|
+
policy: loadPolicy(provisionalContext),
|
|
36
|
+
options,
|
|
37
|
+
interactive: !options.yes,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function analyzeWithResolvedDiscovery(options) {
|
|
42
|
+
let currentOptions = { ...options };
|
|
43
|
+
let lastResult = null;
|
|
44
|
+
let promptSession = null;
|
|
45
|
+
|
|
46
|
+
function getPromptSession() {
|
|
47
|
+
if (currentOptions.yes) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (!promptSession) {
|
|
51
|
+
promptSession = createDiscoveryPromptSession();
|
|
52
|
+
}
|
|
53
|
+
return promptSession;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const engineResolution = await resolveAnalyzeEngineSelection(currentOptions);
|
|
58
|
+
if (!engineResolution.ok) {
|
|
59
|
+
return {
|
|
60
|
+
options: currentOptions,
|
|
61
|
+
result: {
|
|
62
|
+
ok: false,
|
|
63
|
+
code: engineResolution.code,
|
|
64
|
+
summary: engineResolution.message,
|
|
65
|
+
discovery: null,
|
|
66
|
+
engineResolution,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
currentOptions = {
|
|
71
|
+
...currentOptions,
|
|
72
|
+
engineResolution,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
76
|
+
lastResult = await analyzeWorkspace({
|
|
77
|
+
cwd: process.cwd(),
|
|
78
|
+
inputPath: currentOptions.inputPath,
|
|
79
|
+
repoRoot: currentOptions.repoRoot,
|
|
80
|
+
docsPath: currentOptions.docsPath,
|
|
81
|
+
configDirName: currentOptions.configDirName,
|
|
82
|
+
allowNewRepoRoot: currentOptions.allowNewRepoRoot,
|
|
83
|
+
engine: currentOptions.engine,
|
|
84
|
+
engineSource: currentOptions.engineSource,
|
|
85
|
+
engineResolution: currentOptions.engineResolution,
|
|
86
|
+
hostContext: currentOptions.hostContext,
|
|
87
|
+
userRequestText: currentOptions.userRequestText,
|
|
88
|
+
yes: currentOptions.yes,
|
|
89
|
+
selectionSources: currentOptions.selectionSources,
|
|
90
|
+
userIntent: currentOptions.userIntent,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (lastResult.ok) {
|
|
94
|
+
return { options: currentOptions, result: lastResult };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const nextOptions = await resolveDiscoveryFailureInteractively(
|
|
98
|
+
lastResult,
|
|
99
|
+
currentOptions,
|
|
100
|
+
process.cwd(),
|
|
101
|
+
!currentOptions.yes,
|
|
102
|
+
getPromptSession(),
|
|
103
|
+
);
|
|
104
|
+
if (!nextOptions) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
currentOptions = nextOptions;
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
promptSession?.close();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { options: currentOptions, result: lastResult };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function printAnalyzeConfirmation(result, activeOptions) {
|
|
117
|
+
console.log(renderAnalyzeConfirmation(
|
|
118
|
+
result.context,
|
|
119
|
+
result.analysis,
|
|
120
|
+
result.backlog,
|
|
121
|
+
activeOptions,
|
|
122
|
+
result.discovery,
|
|
123
|
+
));
|
|
124
|
+
console.log("");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function resolveRepoConflict(analyzed, result, activeOptions) {
|
|
128
|
+
if (!shouldConfirmRepoRebuild(result.analysis, result.discovery)) {
|
|
129
|
+
return { state: "ready", analyzed, result, activeOptions };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (activeOptions.rebuildExisting) {
|
|
133
|
+
const resetSummary = resetRepoForRebuild(result.context, result.discovery);
|
|
134
|
+
console.log(renderRebuildSummary(resetSummary));
|
|
135
|
+
console.log("");
|
|
136
|
+
return {
|
|
137
|
+
state: "reanalyze",
|
|
138
|
+
options: {
|
|
139
|
+
...activeOptions,
|
|
140
|
+
repoRoot: result.context.repoRoot,
|
|
141
|
+
rebuildExisting: false,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (activeOptions.yes) {
|
|
147
|
+
console.log(renderRepoConflictStopMessage(result.analysis));
|
|
148
|
+
return { state: "exit", exitCode: 1 };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const repoConflictDecision = await confirmRepoConflictResolution(result.analysis);
|
|
152
|
+
if (repoConflictDecision === "cancel") {
|
|
153
|
+
console.log("已取消自动执行;分析结果与 backlog 已保留在 .helloloop/。");
|
|
154
|
+
return { state: "done" };
|
|
155
|
+
}
|
|
156
|
+
if (repoConflictDecision === "continue") {
|
|
157
|
+
return { state: "ready", analyzed, result, activeOptions };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const resetSummary = resetRepoForRebuild(result.context, result.discovery);
|
|
161
|
+
console.log(renderRebuildSummary(resetSummary));
|
|
162
|
+
console.log("");
|
|
163
|
+
return {
|
|
164
|
+
state: "reanalyze",
|
|
165
|
+
options: {
|
|
166
|
+
...activeOptions,
|
|
167
|
+
repoRoot: result.context.repoRoot,
|
|
168
|
+
rebuildExisting: false,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function prepareAnalyzeExecution(initialOptions) {
|
|
174
|
+
let analyzed = await analyzeWithResolvedDiscovery(initialOptions);
|
|
175
|
+
let result = analyzed.result;
|
|
176
|
+
let activeOptions = analyzed.options;
|
|
177
|
+
|
|
178
|
+
while (true) {
|
|
179
|
+
if (!result.ok) {
|
|
180
|
+
console.error(result.summary);
|
|
181
|
+
return { exitCode: 1 };
|
|
182
|
+
}
|
|
183
|
+
if (result.engineResolution?.ok) {
|
|
184
|
+
activeOptions = {
|
|
185
|
+
...activeOptions,
|
|
186
|
+
engineResolution: result.engineResolution,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
printAnalyzeConfirmation(result, activeOptions);
|
|
191
|
+
const conflictResolution = await resolveRepoConflict(analyzed, result, activeOptions);
|
|
192
|
+
if (conflictResolution.state === "ready") {
|
|
193
|
+
return { result, activeOptions };
|
|
194
|
+
}
|
|
195
|
+
if (conflictResolution.state === "done" || conflictResolution.state === "exit") {
|
|
196
|
+
return { exitCode: conflictResolution.exitCode || 0 };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
analyzed = await analyzeWithResolvedDiscovery(conflictResolution.options);
|
|
200
|
+
result = analyzed.result;
|
|
201
|
+
activeOptions = analyzed.options;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function maybeRunAutoExecution(result, activeOptions) {
|
|
206
|
+
const execution = analyzeExecution(result.backlog, activeOptions);
|
|
207
|
+
|
|
208
|
+
if (activeOptions.dryRun) {
|
|
209
|
+
console.log("已按 --dry-run 跳过自动执行。");
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
if (execution.state !== "ready") {
|
|
213
|
+
console.log(renderAnalyzeStopMessage(execution.blockedReason || "当前 backlog 已无可自动执行任务。"));
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const approved = activeOptions.yes ? true : await confirmAutoExecution();
|
|
218
|
+
if (!approved) {
|
|
219
|
+
console.log("已取消自动执行;分析结果与 backlog 已保留在 .helloloop/。");
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.log("");
|
|
224
|
+
console.log("开始自动接续执行...");
|
|
225
|
+
const results = await runLoop(result.context, {
|
|
226
|
+
...activeOptions,
|
|
227
|
+
engineResolution: result.engineResolution?.ok ? result.engineResolution : activeOptions.engineResolution,
|
|
228
|
+
maxTasks: resolveAutoRunMaxTasks(result.backlog, activeOptions) || undefined,
|
|
229
|
+
fullAutoMainline: true,
|
|
230
|
+
});
|
|
231
|
+
const refreshedBacklog = loadBacklog(result.context);
|
|
232
|
+
console.log(renderAutoRunSummary(result.context, refreshedBacklog, results, activeOptions));
|
|
233
|
+
return results.some((item) => !item.ok) ? 1 : 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function handleAnalyzeCommand(options) {
|
|
237
|
+
if (hasBlockingInputIssues(options.inputIssues)) {
|
|
238
|
+
console.error(renderBlockingInputIssueMessage(options.inputIssues));
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const prepared = await prepareAnalyzeExecution(options);
|
|
243
|
+
if (!prepared.result) {
|
|
244
|
+
return prepared.exitCode || 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return maybeRunAutoExecution(prepared.result, prepared.activeOptions);
|
|
248
|
+
}
|
package/src/cli_args.mjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const REPO_ROOT_PLACEHOLDER = "<REPO_ROOT>";
|
|
2
|
+
const DOCS_PATH_PLACEHOLDER = "<DOCS_PATH>";
|
|
3
|
+
const KNOWN_COMMANDS = new Set([
|
|
4
|
+
"analyze",
|
|
5
|
+
"install",
|
|
6
|
+
"uninstall",
|
|
7
|
+
"init",
|
|
8
|
+
"status",
|
|
9
|
+
"next",
|
|
10
|
+
"run-once",
|
|
11
|
+
"run-loop",
|
|
12
|
+
"doctor",
|
|
13
|
+
"help",
|
|
14
|
+
"--help",
|
|
15
|
+
"-h",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export function parseArgs(argv) {
|
|
19
|
+
const [first = "", ...restArgs] = argv;
|
|
20
|
+
const command = !first
|
|
21
|
+
? "analyze"
|
|
22
|
+
: (KNOWN_COMMANDS.has(first) ? first : "analyze");
|
|
23
|
+
const rest = !first
|
|
24
|
+
? []
|
|
25
|
+
: (KNOWN_COMMANDS.has(first) ? restArgs : argv);
|
|
26
|
+
const options = {
|
|
27
|
+
requiredDocs: [],
|
|
28
|
+
constraints: [],
|
|
29
|
+
positionalArgs: [],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
33
|
+
const arg = rest[index];
|
|
34
|
+
if (arg === "--dry-run") options.dryRun = true;
|
|
35
|
+
else if (arg === "--yes" || arg === "-y") options.yes = true;
|
|
36
|
+
else if (arg === "--allow-high-risk") options.allowHighRisk = true;
|
|
37
|
+
else if (arg === "--rebuild-existing") options.rebuildExisting = true;
|
|
38
|
+
else if (arg === "--force") options.force = true;
|
|
39
|
+
else if (arg === "--task-id") { options.taskId = rest[index + 1]; index += 1; }
|
|
40
|
+
else if (arg === "--max-tasks") { options.maxTasks = Number(rest[index + 1]); index += 1; }
|
|
41
|
+
else if (arg === "--max-attempts") { options.maxAttempts = Number(rest[index + 1]); index += 1; }
|
|
42
|
+
else if (arg === "--max-strategies") { options.maxStrategies = Number(rest[index + 1]); index += 1; }
|
|
43
|
+
else if (arg === "--repo") { options.repoRoot = rest[index + 1]; index += 1; }
|
|
44
|
+
else if (arg === "--docs") { options.docsPath = rest[index + 1]; index += 1; }
|
|
45
|
+
else if (arg === "--host") { options.host = rest[index + 1]; index += 1; }
|
|
46
|
+
else if (arg === "--engine") { options.engine = rest[index + 1]; options.engineSource = "flag"; index += 1; }
|
|
47
|
+
else if (arg === "--host-context") { options.hostContext = rest[index + 1]; index += 1; }
|
|
48
|
+
else if (arg === "--codex-home") { options.codexHome = rest[index + 1]; index += 1; }
|
|
49
|
+
else if (arg === "--claude-home") { options.claudeHome = rest[index + 1]; index += 1; }
|
|
50
|
+
else if (arg === "--gemini-home") { options.geminiHome = rest[index + 1]; index += 1; }
|
|
51
|
+
else if (arg === "--config-dir") { options.configDirName = rest[index + 1]; index += 1; }
|
|
52
|
+
else if (arg === "--required-doc") { options.requiredDocs.push(rest[index + 1]); index += 1; }
|
|
53
|
+
else if (arg === "--constraint") { options.constraints.push(rest[index + 1]); index += 1; }
|
|
54
|
+
else { options.positionalArgs.push(arg); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { command, options };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function helpText() {
|
|
61
|
+
return [
|
|
62
|
+
"用法:helloloop [command] [engine] [path|需求说明...] [options]",
|
|
63
|
+
"",
|
|
64
|
+
"命令:",
|
|
65
|
+
" analyze 自动分析并生成执行确认单;确认后继续自动接续开发(默认)",
|
|
66
|
+
" install 安装插件到 Codex Home(适合 npx / npm bin 分发)",
|
|
67
|
+
" uninstall 从所选宿主卸载插件并清理注册信息",
|
|
68
|
+
" init 初始化 .helloloop 配置",
|
|
69
|
+
" status 查看 backlog 与下一任务",
|
|
70
|
+
" next 生成下一任务干跑预览",
|
|
71
|
+
" run-once 执行一个任务",
|
|
72
|
+
" run-loop 连续执行多个任务",
|
|
73
|
+
" doctor 检查 Codex、当前插件 bundle 与目标仓库 .helloloop 配置是否可用",
|
|
74
|
+
"",
|
|
75
|
+
"选项:",
|
|
76
|
+
" --host <name> 安装宿主:codex | claude | gemini | all(默认 codex)",
|
|
77
|
+
" --codex-home <dir> Codex Home,install 默认使用 ~/.codex",
|
|
78
|
+
" --claude-home <dir> Claude Home,install 默认使用 ~/.claude",
|
|
79
|
+
" --gemini-home <dir> Gemini Home,install 默认使用 ~/.gemini",
|
|
80
|
+
" [engine] analyze 默认支持直接写:codex | claude | gemini",
|
|
81
|
+
" --repo <dir> 高级选项:显式指定项目仓库根目录",
|
|
82
|
+
" --docs <dir|file> 高级选项:显式指定开发文档目录或文件",
|
|
83
|
+
" --config-dir <dir> 配置目录,默认 .helloloop",
|
|
84
|
+
" -y, --yes 跳过交互确认,分析后直接开始自动执行",
|
|
85
|
+
" --dry-run 只分析并输出确认单,不真正开始自动执行",
|
|
86
|
+
" --task-id <id> 指定任务 id",
|
|
87
|
+
" --max-tasks <n> run-loop 最多执行 n 个任务",
|
|
88
|
+
" --max-attempts <n> 每种策略内最多重试 n 次",
|
|
89
|
+
" --max-strategies <n> 单任务最多切换 n 种策略继续重试",
|
|
90
|
+
" --allow-high-risk 允许执行 medium/high/critical 风险任务",
|
|
91
|
+
" --rebuild-existing 分析判断当前项目与文档冲突时,自动清理当前项目后按文档重建",
|
|
92
|
+
" --required-doc <p> 增加一个全局必读文档(AGENTS.md 会被自动忽略)",
|
|
93
|
+
" --constraint <text> 增加一个全局实现约束",
|
|
94
|
+
"",
|
|
95
|
+
"补充说明:",
|
|
96
|
+
" analyze 默认支持在命令后混合传入引擎、路径和自然语言要求。",
|
|
97
|
+
" 如果同时检测到多个可用引擎且没有明确指定,会先询问你选择。",
|
|
98
|
+
" 示例:npx helloloop claude <DOCS_PATH> <PROJECT_ROOT> 先分析偏差,不要执行",
|
|
99
|
+
].join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function printHelp() {
|
|
103
|
+
console.log(helpText());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { DOCS_PATH_PLACEHOLDER, REPO_ROOT_PLACEHOLDER };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { createContext } from "./context.mjs";
|
|
4
|
+
import { loadBacklog, scaffoldIfMissing } from "./config.mjs";
|
|
5
|
+
import { installPluginBundle, uninstallPluginBundle } from "./install.mjs";
|
|
6
|
+
import { runLoop, runOnce, renderStatusText } from "./runner.mjs";
|
|
7
|
+
import { renderInstallSummary, renderUninstallSummary } from "./cli_render.mjs";
|
|
8
|
+
|
|
9
|
+
export function handleInstallCommand(options) {
|
|
10
|
+
const context = createContext({
|
|
11
|
+
repoRoot: options.repoRoot,
|
|
12
|
+
configDirName: options.configDirName,
|
|
13
|
+
});
|
|
14
|
+
const result = installPluginBundle({
|
|
15
|
+
bundleRoot: context.bundleRoot,
|
|
16
|
+
host: options.host,
|
|
17
|
+
codexHome: options.codexHome,
|
|
18
|
+
claudeHome: options.claudeHome,
|
|
19
|
+
geminiHome: options.geminiHome,
|
|
20
|
+
force: options.force,
|
|
21
|
+
});
|
|
22
|
+
console.log(renderInstallSummary(result));
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function handleUninstallCommand(options) {
|
|
27
|
+
const result = uninstallPluginBundle({
|
|
28
|
+
host: options.host,
|
|
29
|
+
codexHome: options.codexHome,
|
|
30
|
+
claudeHome: options.claudeHome,
|
|
31
|
+
geminiHome: options.geminiHome,
|
|
32
|
+
});
|
|
33
|
+
console.log(renderUninstallSummary(result));
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function handleInitCommand(context) {
|
|
38
|
+
const created = scaffoldIfMissing(context);
|
|
39
|
+
if (!created.length) {
|
|
40
|
+
console.log("HelloLoop 配置已存在,无需初始化。");
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log([
|
|
45
|
+
"已初始化以下文件:",
|
|
46
|
+
...created.map((item) => `- ${path.relative(context.repoRoot, item).replaceAll("\\", "/")}`),
|
|
47
|
+
].join("\n"));
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function handleDoctorCommand(context, options, runDoctor) {
|
|
52
|
+
await runDoctor(context, options);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function handleStatusCommand(context, options) {
|
|
57
|
+
console.log(renderStatusText(context, options));
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function handleNextCommand(context, options) {
|
|
62
|
+
const result = await runOnce(context, { ...options, dryRun: true });
|
|
63
|
+
if (!result.task) {
|
|
64
|
+
console.log("当前没有可执行任务。");
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log([
|
|
69
|
+
"下一任务预览",
|
|
70
|
+
"============",
|
|
71
|
+
`任务:${result.task.title}`,
|
|
72
|
+
`编号:${result.task.id}`,
|
|
73
|
+
`运行目录:${result.runDir}`,
|
|
74
|
+
"",
|
|
75
|
+
"验证命令:",
|
|
76
|
+
...result.verifyCommands.map((item) => `- ${item}`),
|
|
77
|
+
"",
|
|
78
|
+
"提示词:",
|
|
79
|
+
result.prompt,
|
|
80
|
+
].join("\n"));
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function handleRunOnceCommand(context, options) {
|
|
85
|
+
const result = await runOnce(context, options);
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
console.error(result.summary || "执行失败。");
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
if (options.dryRun) {
|
|
91
|
+
console.log(result.task
|
|
92
|
+
? `已生成干跑预览:${result.task.title}\n运行目录:${result.runDir}`
|
|
93
|
+
: "当前没有可执行任务。");
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(result.task
|
|
98
|
+
? `完成任务:${result.task.title}\n运行目录:${result.runDir}`
|
|
99
|
+
: "没有可执行任务。");
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function handleRunLoopCommand(context, options) {
|
|
104
|
+
const results = await runLoop(context, options);
|
|
105
|
+
const failed = results.find((item) => !item.ok);
|
|
106
|
+
|
|
107
|
+
for (const item of results) {
|
|
108
|
+
if (!item.task) {
|
|
109
|
+
console.log("没有更多可执行任务。");
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
console.log(`${item.ok ? "成功" : "失败"}:${item.task.title}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (failed) {
|
|
116
|
+
console.error(failed.summary || "连续执行中断。");
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createContext } from "./context.mjs";
|
|
2
|
+
import { resolveRepoRoot } from "./discovery.mjs";
|
|
3
|
+
|
|
4
|
+
export function resolveContextFromOptions(options) {
|
|
5
|
+
const resolvedRepo = resolveRepoRoot({
|
|
6
|
+
cwd: process.cwd(),
|
|
7
|
+
repoRoot: options.repoRoot,
|
|
8
|
+
inputPath: options.inputPath,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
if (!resolvedRepo.ok) {
|
|
12
|
+
throw new Error(resolvedRepo.message);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return createContext({
|
|
16
|
+
repoRoot: resolvedRepo.repoRoot,
|
|
17
|
+
configDirName: options.configDirName,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveStandardCommandOptions(options) {
|
|
22
|
+
const nextOptions = { ...options };
|
|
23
|
+
const positionals = Array.isArray(nextOptions.positionalArgs) ? nextOptions.positionalArgs : [];
|
|
24
|
+
if (positionals.length > 1) {
|
|
25
|
+
throw new Error(`未知参数:${positionals.slice(1).join(" ")}`);
|
|
26
|
+
}
|
|
27
|
+
if (positionals.length === 1 && !nextOptions.inputPath) {
|
|
28
|
+
nextOptions.inputPath = positionals[0];
|
|
29
|
+
}
|
|
30
|
+
return nextOptions;
|
|
31
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { DOCS_PATH_PLACEHOLDER, REPO_ROOT_PLACEHOLDER } from "./cli_args.mjs";
|
|
2
|
+
|
|
3
|
+
function renderFollowupExamples() {
|
|
4
|
+
return [
|
|
5
|
+
"下一步示例:",
|
|
6
|
+
"npx helloloop",
|
|
7
|
+
"npx helloloop <PATH>",
|
|
8
|
+
"npx helloloop codex",
|
|
9
|
+
"npx helloloop claude <PATH>",
|
|
10
|
+
"npx helloloop gemini <PATH> 继续完成后续开发",
|
|
11
|
+
"npx helloloop --dry-run",
|
|
12
|
+
"npx helloloop install --host all",
|
|
13
|
+
"npx helloloop uninstall --host all",
|
|
14
|
+
"npx helloloop next",
|
|
15
|
+
`如需显式补充路径:npx helloloop --repo ${REPO_ROOT_PLACEHOLDER} --docs ${DOCS_PATH_PLACEHOLDER}`,
|
|
16
|
+
].join("\n");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function renderInstallSummary(result) {
|
|
20
|
+
const lines = ["HelloLoop 已安装到以下宿主:"];
|
|
21
|
+
|
|
22
|
+
for (const item of result.installedHosts) {
|
|
23
|
+
lines.push(`- ${item.displayName}:${item.targetRoot}`);
|
|
24
|
+
if (item.marketplaceFile) {
|
|
25
|
+
lines.push(` marketplace:${item.marketplaceFile}`);
|
|
26
|
+
}
|
|
27
|
+
if (item.settingsFile) {
|
|
28
|
+
lines.push(` settings:${item.settingsFile}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
lines.push("");
|
|
33
|
+
lines.push("使用入口:");
|
|
34
|
+
lines.push("- Codex:`$helloloop` / `npx helloloop`");
|
|
35
|
+
lines.push("- Claude:`/helloloop`");
|
|
36
|
+
lines.push("- Gemini:`/helloloop`");
|
|
37
|
+
lines.push("");
|
|
38
|
+
lines.push(renderFollowupExamples());
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function renderUninstallSummary(result) {
|
|
43
|
+
const lines = ["HelloLoop 已从以下宿主卸载:"];
|
|
44
|
+
|
|
45
|
+
for (const item of result.uninstalledHosts) {
|
|
46
|
+
lines.push(`- ${item.displayName}:${item.removed ? "已清理" : "未发现现有安装"}`);
|
|
47
|
+
lines.push(` 目标目录:${item.targetRoot}`);
|
|
48
|
+
if (item.marketplaceFile) {
|
|
49
|
+
lines.push(` marketplace:${item.marketplaceFile}`);
|
|
50
|
+
}
|
|
51
|
+
if (item.settingsFile) {
|
|
52
|
+
lines.push(` settings:${item.settingsFile}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
lines.push("");
|
|
57
|
+
lines.push("如需重新安装:");
|
|
58
|
+
lines.push("- `npx helloloop install --host codex`");
|
|
59
|
+
lines.push("- `npx helloloop install --host all`");
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function renderRebuildSummary(resetSummary) {
|
|
64
|
+
return [
|
|
65
|
+
"已按确认结果清理当前项目,并准备按开发文档重新开始。",
|
|
66
|
+
`- 已清理顶层条目:${resetSummary.removedEntries.length ? resetSummary.removedEntries.join(",") : "无"}`,
|
|
67
|
+
`- 已保留开发文档:${resetSummary.preservedDocs.length ? resetSummary.preservedDocs.join(",") : "无"}`,
|
|
68
|
+
`- 重建记录:${resetSummary.manifestFile.replaceAll("\\", "/")}`,
|
|
69
|
+
].join("\n");
|
|
70
|
+
}
|
package/src/cli_support.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { createInterface } from "node:readline/promises";
|
|
4
3
|
|
|
5
4
|
import { analyzeExecution, summarizeBacklog } from "./backlog.mjs";
|
|
6
5
|
import { fileExists, readJson } from "./common.mjs";
|
|
6
|
+
import { createPromptSession } from "./prompt_session.mjs";
|
|
7
7
|
import { resolveCliInvocation, resolveCodexInvocation } from "./shell_invocation.mjs";
|
|
8
8
|
|
|
9
9
|
function probeCodexVersion() {
|
|
@@ -288,16 +288,13 @@ function isAffirmativeAnswer(answer) {
|
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
export async function confirmAutoExecution() {
|
|
291
|
-
const
|
|
292
|
-
input: process.stdin,
|
|
293
|
-
output: process.stdout,
|
|
294
|
-
});
|
|
291
|
+
const promptSession = createPromptSession();
|
|
295
292
|
|
|
296
293
|
try {
|
|
297
|
-
const answer = await
|
|
294
|
+
const answer = await promptSession.question("是否开始自动接续执行?输入 y / yes / 确认 继续,其它任意输入取消:");
|
|
298
295
|
return isAffirmativeAnswer(answer);
|
|
299
296
|
} finally {
|
|
300
|
-
|
|
297
|
+
promptSession.close();
|
|
301
298
|
}
|
|
302
299
|
}
|
|
303
300
|
|
|
@@ -308,10 +305,7 @@ export function shouldConfirmRepoRebuild(analysis, discovery) {
|
|
|
308
305
|
|
|
309
306
|
export async function confirmRepoConflictResolution(analysis) {
|
|
310
307
|
const decision = analysis?.repoDecision || {};
|
|
311
|
-
const
|
|
312
|
-
input: process.stdin,
|
|
313
|
-
output: process.stdout,
|
|
314
|
-
});
|
|
308
|
+
const promptSession = createPromptSession();
|
|
315
309
|
|
|
316
310
|
const promptText = [
|
|
317
311
|
"检测到当前项目与开发文档目标存在明显冲突:",
|
|
@@ -324,7 +318,7 @@ export async function confirmRepoConflictResolution(analysis) {
|
|
|
324
318
|
].join("\n");
|
|
325
319
|
|
|
326
320
|
try {
|
|
327
|
-
const answer = String(await
|
|
321
|
+
const answer = String(await promptSession.question(promptText) || "").trim();
|
|
328
322
|
if (["2", "重建", "rebuild"].includes(answer.toLowerCase ? answer.toLowerCase() : answer)) {
|
|
329
323
|
return "rebuild";
|
|
330
324
|
}
|
|
@@ -333,7 +327,7 @@ export async function confirmRepoConflictResolution(analysis) {
|
|
|
333
327
|
}
|
|
334
328
|
return "cancel";
|
|
335
329
|
} finally {
|
|
336
|
-
|
|
330
|
+
promptSession.close();
|
|
337
331
|
}
|
|
338
332
|
}
|
|
339
333
|
|
|
@@ -352,6 +346,7 @@ export function renderAnalyzeStopMessage(reason) {
|
|
|
352
346
|
export function renderAutoRunSummary(context, backlog, results, options = {}) {
|
|
353
347
|
const summary = summarizeBacklog(backlog);
|
|
354
348
|
const execution = analyzeExecution(backlog, options);
|
|
349
|
+
const mainlineClosed = results.some((item) => item.kind === "mainline-complete");
|
|
355
350
|
const lines = [
|
|
356
351
|
"自动执行结果",
|
|
357
352
|
"============",
|
|
@@ -383,7 +378,9 @@ export function renderAutoRunSummary(context, backlog, results, options = {}) {
|
|
|
383
378
|
lines.push("");
|
|
384
379
|
lines.push("结论:");
|
|
385
380
|
if (execution.state === "done") {
|
|
386
|
-
lines.push(
|
|
381
|
+
lines.push(mainlineClosed
|
|
382
|
+
? "- backlog 已全部完成,且主线终态复核通过"
|
|
383
|
+
: "- backlog 已全部完成");
|
|
387
384
|
} else if (execution.blockedReason) {
|
|
388
385
|
lines.push(`- 当前停止原因:${execution.blockedReason}`);
|
|
389
386
|
} else {
|