dsh-easygit-plugin 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/LICENSE +21 -0
- package/README.md +131 -0
- package/README.zh-CN.md +131 -0
- package/SECURITY.md +23 -0
- package/assets/preview.png +0 -0
- package/git-guide.cordis.yml +14 -0
- package/lib/client.js +2064 -0
- package/lib/index.js +1705 -0
- package/lib/types/client/index.d.ts +34 -0
- package/lib/types/client/panel-controller.d.ts +38 -0
- package/lib/types/client/view-model.d.ts +99 -0
- package/lib/types/host/actions.d.ts +41 -0
- package/lib/types/host/command-policy.d.ts +38 -0
- package/lib/types/host/git-repository-service.d.ts +53 -0
- package/lib/types/host/index.d.ts +3 -0
- package/lib/types/host/plugin.d.ts +109 -0
- package/lib/types/host/proposal-service.d.ts +60 -0
- package/lib/types/shared/contracts.d.ts +282 -0
- package/package.json +79 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1705 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
7
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// src/host/command-policy.ts
|
|
11
|
+
function quoteShellArg(value) {
|
|
12
|
+
return "'" + String(value).replace(/'/g, "'\\''") + "'";
|
|
13
|
+
}
|
|
14
|
+
function parseCommand(command) {
|
|
15
|
+
if (typeof command !== "string" || command.trim().length === 0) return { ok: false, error: "\u547D\u4EE4\u4E3A\u7A7A" };
|
|
16
|
+
if (command.length > MAX_COMMAND_LENGTH) return { ok: false, error: "\u547D\u4EE4\u8FC7\u957F\uFF08\u6700\u591A 800 \u5B57\u7B26\uFF09" };
|
|
17
|
+
if (/\0|\r|\n/.test(command)) return { ok: false, error: "\u6BCF\u4E2A\u6B65\u9AA4\u53EA\u80FD\u5305\u542B\u4E00\u6761\u547D\u4EE4\uFF0C\u591A\u6B65\u64CD\u4F5C\u8BF7\u4F7F\u7528 steps \u6570\u7EC4" };
|
|
18
|
+
const args = [];
|
|
19
|
+
let current = "";
|
|
20
|
+
let quote = null;
|
|
21
|
+
let started = false;
|
|
22
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
23
|
+
const character = command[index] ?? "";
|
|
24
|
+
if (quote === "'") {
|
|
25
|
+
if (character === "'") quote = null;
|
|
26
|
+
else current += character;
|
|
27
|
+
started = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (quote === '"') {
|
|
31
|
+
if (character === '"') {
|
|
32
|
+
quote = null;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (character === "$" || character === "`") return { ok: false, error: "\u53CC\u5F15\u53F7\u5185\u4E0D\u5141\u8BB8 shell \u5C55\u5F00\uFF08$ \u6216\u53CD\u5F15\u53F7\uFF09" };
|
|
36
|
+
if (character === "\\") {
|
|
37
|
+
if (index + 1 >= command.length) return { ok: false, error: "\u547D\u4EE4\u672B\u5C3E\u5B58\u5728\u4E0D\u5B8C\u6574\u7684\u8F6C\u4E49" };
|
|
38
|
+
const next = command[index += 1] ?? "";
|
|
39
|
+
current += ['"', "\\", "$", "`"].includes(next) ? next : "\\" + next;
|
|
40
|
+
} else current += character;
|
|
41
|
+
started = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (/\s/.test(character)) {
|
|
45
|
+
if (started) {
|
|
46
|
+
args.push(current);
|
|
47
|
+
current = "";
|
|
48
|
+
started = false;
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (character === "'" || character === '"') {
|
|
53
|
+
quote = character;
|
|
54
|
+
started = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (character === "\\") {
|
|
58
|
+
if (index + 1 >= command.length) return { ok: false, error: "\u547D\u4EE4\u672B\u5C3E\u5B58\u5728\u4E0D\u5B8C\u6574\u7684\u8F6C\u4E49" };
|
|
59
|
+
current += command[index += 1] ?? "";
|
|
60
|
+
started = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (";&|<>".includes(character)) return { ok: false, error: "\u547D\u4EE4\u5305\u542B shell \u63A7\u5236\u7B26\uFF08; & | < >\uFF09\uFF0C\u591A\u6B65\u64CD\u4F5C\u8BF7\u4F7F\u7528 steps \u6570\u7EC4" };
|
|
64
|
+
if (character === "$" || character === "`") return { ok: false, error: "\u547D\u4EE4\u5305\u542B shell \u5C55\u5F00\uFF08$ \u6216\u53CD\u5F15\u53F7\uFF09" };
|
|
65
|
+
current += character;
|
|
66
|
+
started = true;
|
|
67
|
+
}
|
|
68
|
+
if (quote) return { ok: false, error: "\u547D\u4EE4\u5305\u542B\u672A\u95ED\u5408\u7684\u5F15\u53F7" };
|
|
69
|
+
if (started) args.push(current);
|
|
70
|
+
if (args.length < 2 || args[0] !== "git") return { ok: false, error: "\u53EA\u5141\u8BB8\u201Cgit <\u5B50\u547D\u4EE4> ...\u201D\u683C\u5F0F" };
|
|
71
|
+
if (args.length > 80) return { ok: false, error: "\u547D\u4EE4\u53C2\u6570\u8FC7\u591A\uFF08\u6700\u591A 80 \u4E2A\uFF09" };
|
|
72
|
+
const subcommand = args[1];
|
|
73
|
+
if (subcommand.startsWith("-")) return { ok: false, error: "\u4E0D\u5141\u8BB8 git \u5168\u5C40\u9009\u9879\uFF08\u5982 -c\u3001-C\u3001--exec-path\uFF09\uFF1B\u8BF7\u901A\u8FC7 workdir \u6307\u5B9A\u4ED3\u5E93" };
|
|
74
|
+
if (!allowedSubcommands.has(subcommand)) {
|
|
75
|
+
return { ok: false, error: "\u4E0D\u652F\u6301 git \u5B50\u547D\u4EE4\u300C" + subcommand + "\u300D\uFF1B\u8BE5\u9650\u5236\u7528\u4E8E\u963B\u6B62 alias\u3001\u5916\u90E8 git-* \u7A0B\u5E8F\u548C\u53EF\u6267\u884C\u811A\u672C\u5165\u53E3" };
|
|
76
|
+
}
|
|
77
|
+
const forbiddenOptions = ["--ext-diff", "--textconv", "--open-files-in-pager", "--upload-pack", "--receive-pack"];
|
|
78
|
+
for (const argument of args.slice(2)) {
|
|
79
|
+
if (forbiddenOptions.some((option) => argument === option || argument.startsWith(option + "="))) {
|
|
80
|
+
return { ok: false, error: "\u4E0D\u5141\u8BB8\u53EF\u80FD\u542F\u52A8\u5916\u90E8\u7A0B\u5E8F\u7684\u9009\u9879\u300C" + argument + "\u300D" };
|
|
81
|
+
}
|
|
82
|
+
if (/^[a-z][a-z0-9+.-]*:\/\/[^/@\s]+@/i.test(argument)) {
|
|
83
|
+
return { ok: false, error: "\u8FDC\u7A0B URL \u4E0D\u5F97\u5185\u5D4C\u7528\u6237\u540D\u3001\u4EE4\u724C\u6216\u5BC6\u7801\uFF0C\u8BF7\u4F7F\u7528\u51ED\u636E\u7BA1\u7406\u5668" };
|
|
84
|
+
}
|
|
85
|
+
if (/^ext::/i.test(argument)) return { ok: false, error: "\u4E0D\u5141\u8BB8 ext:: \u8FDC\u7A0B\u52A9\u624B\u6267\u884C\u5916\u90E8\u547D\u4EE4" };
|
|
86
|
+
if (/AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|npm_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9_-]{20,}/.test(argument)) {
|
|
87
|
+
return { ok: false, error: "\u547D\u4EE4\u7591\u4F3C\u5305\u542B\u8BBF\u95EE\u5BC6\u94A5\u6216\u4EE4\u724C\uFF0C\u5DF2\u62D2\u7EDD\u767B\u8BB0" };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const tail = args.slice(2);
|
|
91
|
+
if (["merge", "pull", "rebase", "cherry-pick", "revert"].includes(subcommand) && tail.some((argument) => /^-s(?:.+)?$/.test(argument) || argument.startsWith("--strategy=") || argument === "--strategy")) {
|
|
92
|
+
return { ok: false, error: "\u4E0D\u5141\u8BB8\u9009\u62E9\u81EA\u5B9A\u4E49 merge strategy\uFF0C\u4EE5\u514D\u542F\u52A8\u5916\u90E8 git-merge-* \u7A0B\u5E8F" };
|
|
93
|
+
}
|
|
94
|
+
if (subcommand === "push" && tail.some((argument) => argument === "--exec" || argument.startsWith("--exec="))) {
|
|
95
|
+
return { ok: false, error: "\u4E0D\u5141\u8BB8 git push --exec \u6307\u5B9A\u8FDC\u7AEF\u63A5\u6536\u7A0B\u5E8F" };
|
|
96
|
+
}
|
|
97
|
+
if (subcommand === "grep" && tail.some((argument) => argument === "-O" || argument.startsWith("-O"))) {
|
|
98
|
+
return { ok: false, error: "\u4E0D\u5141\u8BB8 git grep -O \u542F\u52A8 pager \u7A0B\u5E8F" };
|
|
99
|
+
}
|
|
100
|
+
if (subcommand === "cat-file" && tail.some((argument) => argument === "--filters" || argument.startsWith("--filters="))) {
|
|
101
|
+
return { ok: false, error: "\u4E0D\u5141\u8BB8 git cat-file --filters \u542F\u52A8\u5185\u5BB9\u8FC7\u6EE4\u7A0B\u5E8F" };
|
|
102
|
+
}
|
|
103
|
+
if (subcommand === "rebase" && tail.some((argument) => argument === "-x" || argument.startsWith("-x") || argument === "--exec" || argument.startsWith("--exec="))) {
|
|
104
|
+
return { ok: false, error: "\u4E0D\u5141\u8BB8 git rebase --exec/-x \u6267\u884C\u4EFB\u610F shell \u547D\u4EE4" };
|
|
105
|
+
}
|
|
106
|
+
return { ok: true, args, subcommand, normalized: args.map(quoteShellArg).join(" ") };
|
|
107
|
+
}
|
|
108
|
+
function validateCommand(command) {
|
|
109
|
+
const parsed = parseCommand(command);
|
|
110
|
+
return parsed.ok ? { ...parsed, segments: [String(command).trim()] } : parsed;
|
|
111
|
+
}
|
|
112
|
+
function displayCommand(args) {
|
|
113
|
+
return args.map((argument) => /^[A-Za-z0-9_@%+=:,./~-]+$/.test(argument) && !argument.startsWith("~") ? argument : quoteShellArg(argument)).join(" ");
|
|
114
|
+
}
|
|
115
|
+
function modernizeCommand(command) {
|
|
116
|
+
const original = typeof command === "string" ? command.trim() : String(command ?? "");
|
|
117
|
+
const parsed = parseCommand(command);
|
|
118
|
+
if (!parsed.ok || parsed.subcommand !== "checkout") return { command: original, changed: false };
|
|
119
|
+
const tail = parsed.args.slice(2);
|
|
120
|
+
if ((tail[0] === "-b" || tail[0] === "-B") && (tail.length === 2 || tail.length === 3)) {
|
|
121
|
+
return {
|
|
122
|
+
command: displayCommand(["git", "switch", tail[0] === "-b" ? "-c" : "-C", ...tail.slice(1)]),
|
|
123
|
+
changed: true
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (tail[0] === "--" && tail.length > 1) {
|
|
127
|
+
return { command: displayCommand(["git", "restore", ...tail]), changed: true };
|
|
128
|
+
}
|
|
129
|
+
if (tail.length > 2 && tail[1] === "--" && !tail[0].startsWith("-")) {
|
|
130
|
+
return {
|
|
131
|
+
command: displayCommand(["git", "restore", "--source=" + tail[0], ...tail.slice(1)]),
|
|
132
|
+
changed: true
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return { command: original, changed: false };
|
|
136
|
+
}
|
|
137
|
+
function classifyRisk(command) {
|
|
138
|
+
const parsed = parseCommand(command);
|
|
139
|
+
if (!parsed.ok) return { level: "hard", reasons: ["\u547D\u4EE4\u65E0\u6CD5\u901A\u8FC7\u5B89\u5168\u89E3\u6790\uFF1A" + parsed.error] };
|
|
140
|
+
const { args, subcommand } = parsed;
|
|
141
|
+
const tail = args.slice(2);
|
|
142
|
+
const reasons = [];
|
|
143
|
+
const hasLong = (name) => tail.some((argument) => argument === name || argument.startsWith(name + "="));
|
|
144
|
+
const hasShort = (letter) => tail.some((argument) => /^-[^-]/.test(argument) && argument.slice(1).includes(letter));
|
|
145
|
+
if (subcommand === "reset") reasons.push(hasLong("--hard") ? "git reset --hard \u4F1A\u4E22\u5F03\u5DE5\u4F5C\u533A\u672A\u63D0\u4EA4\u7684\u6539\u52A8" : "git reset \u53EF\u80FD\u79FB\u52A8\u5206\u652F\u5386\u53F2\u6216\u91CD\u7F6E\u7D22\u5F15");
|
|
146
|
+
if (subcommand === "clean" && !hasShort("n") && !hasLong("--dry-run")) reasons.push("git clean \u4F1A\u6C38\u4E45\u5220\u9664\u672A\u8DDF\u8E2A\u7684\u6587\u4EF6");
|
|
147
|
+
if (subcommand === "push") {
|
|
148
|
+
if (hasShort("f") || hasLong("--force") || hasLong("--force-with-lease") || hasLong("--force-if-includes") || tail.some((argument) => argument.startsWith("+"))) reasons.push("\u5F3A\u5236\u63A8\u9001\u4F1A\u8986\u76D6\u8FDC\u7A0B\u5206\u652F\u5386\u53F2");
|
|
149
|
+
if (hasShort("d") || hasLong("--delete") || hasLong("--mirror") || hasLong("--prune") || tail.some((argument) => argument.startsWith(":"))) reasons.push("\u8BE5 push \u53EF\u80FD\u5220\u9664\u8FDC\u7A0B\u5F15\u7528");
|
|
150
|
+
}
|
|
151
|
+
if (subcommand === "rebase") reasons.push("rebase \u4F1A\u91CD\u5199\u63D0\u4EA4\u5386\u53F2");
|
|
152
|
+
if (subcommand === "pull" && hasLong("--rebase")) reasons.push("pull --rebase \u4F1A\u91CD\u5199\u672C\u5730\u63D0\u4EA4\u5386\u53F2");
|
|
153
|
+
if (subcommand === "branch" && (hasShort("d") || hasShort("D") || hasShort("f") || hasShort("M") || hasLong("--delete") || hasLong("--force"))) reasons.push("\u79FB\u52A8\u3001\u8986\u76D6\u6216\u5220\u9664\u672C\u5730\u5206\u652F\u53EF\u80FD\u4E22\u5931\u63D0\u4EA4\u5F15\u7528");
|
|
154
|
+
if (subcommand === "checkout") reasons.push("checkout \u53EF\u80FD\u5207\u6362\u5206\u652F\u3001\u79FB\u52A8\u5206\u652F\u5F15\u7528\u6216\u8986\u76D6\u5DE5\u4F5C\u533A\u6587\u4EF6\uFF1B\u5EFA\u8BAE\u4F18\u5148\u4F7F\u7528 switch/restore");
|
|
155
|
+
if (subcommand === "switch" && (hasShort("f") || hasShort("C") || hasLong("--force") || hasLong("--force-create") || hasLong("--discard-changes"))) reasons.push("switch \u4F1A\u4E22\u5F03\u5DE5\u4F5C\u533A\u6539\u52A8\u6216\u5F3A\u5236\u79FB\u52A8\u5206\u652F");
|
|
156
|
+
if (subcommand === "restore" && (!hasLong("--staged") || hasLong("--worktree"))) reasons.push("git restore \u4F1A\u4E22\u5F03\u5DE5\u4F5C\u533A\u6539\u52A8");
|
|
157
|
+
if (subcommand === "rm") reasons.push("git rm \u4F1A\u5220\u9664\u5DE5\u4F5C\u533A\u6587\u4EF6\u5E76\u6682\u5B58\u5220\u9664\u64CD\u4F5C");
|
|
158
|
+
if (subcommand === "stash" && (tail[0] === "drop" || tail[0] === "clear")) reasons.push("\u4F1A\u5220\u9664 stash \u8BB0\u5F55");
|
|
159
|
+
if (subcommand === "commit" && hasLong("--amend")) reasons.push("commit --amend \u4F1A\u91CD\u5199\u6700\u8FD1\u4E00\u6B21\u63D0\u4EA4");
|
|
160
|
+
if (subcommand === "reflog" && (tail[0] === "delete" || tail[0] === "expire")) reasons.push("\u4F1A\u5220\u9664\u6216\u8FC7\u671F reflog \u6062\u590D\u8BB0\u5F55");
|
|
161
|
+
if (subcommand === "tag" && (hasShort("d") || hasLong("--delete"))) reasons.push("\u5220\u9664 tag \u4F1A\u79FB\u9664\u63D0\u4EA4\u6807\u7B7E");
|
|
162
|
+
if (subcommand === "tag" && (hasShort("f") || hasLong("--force"))) reasons.push("\u5F3A\u5236\u66F4\u65B0 tag \u4F1A\u79FB\u52A8\u5DF2\u6709\u6807\u7B7E");
|
|
163
|
+
return reasons.length > 0 ? { level: "hard", reasons } : safeSubcommands.has(subcommand) ? { level: "safe", reasons: [] } : { level: "normal", reasons: [] };
|
|
164
|
+
}
|
|
165
|
+
function classifyStepsRisk(commands) {
|
|
166
|
+
let level = "safe";
|
|
167
|
+
const reasons = [];
|
|
168
|
+
for (const command of commands) {
|
|
169
|
+
const risk = classifyRisk(command);
|
|
170
|
+
if (risk.level === "hard") level = "hard";
|
|
171
|
+
else if (risk.level === "normal" && level === "safe") level = "normal";
|
|
172
|
+
for (const reason of risk.reasons) if (!reasons.includes(reason)) reasons.push(reason);
|
|
173
|
+
}
|
|
174
|
+
return { level, reasons };
|
|
175
|
+
}
|
|
176
|
+
function redactSecrets(value) {
|
|
177
|
+
return String(value || "").replace(/(\b[a-z][a-z0-9+.-]{0,19}:\/\/)[^/@\s]+@/gi, "$1***@").replace(/\bAKIA[0-9A-Z]{16}\b/g, "[REDACTED_AWS_KEY]").replace(/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, "[REDACTED_GITHUB_TOKEN]").replace(/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, "[REDACTED_GITHUB_TOKEN]").replace(/\bglpat-[A-Za-z0-9_-]{20,}\b/g, "[REDACTED_GITLAB_TOKEN]").replace(/\bnpm_[A-Za-z0-9]{20,}\b/g, "[REDACTED_NPM_TOKEN]").replace(/\bsk-[A-Za-z0-9_-]{20,}\b/g, "[REDACTED_API_KEY]").replace(/([?&](?:access_token|auth|password|token)=)[^&#\s]+/gi, "$1[REDACTED]");
|
|
178
|
+
}
|
|
179
|
+
function redactAndLimit(value, maxChars = 2e4) {
|
|
180
|
+
const redacted = redactSecrets(value);
|
|
181
|
+
return redacted.length <= maxChars ? redacted : redacted.slice(0, maxChars) + "\n\u2026[\u8F93\u51FA\u5DF2\u622A\u65AD]";
|
|
182
|
+
}
|
|
183
|
+
function addPathsOf(command) {
|
|
184
|
+
const parsed = parseCommand(command);
|
|
185
|
+
if (!parsed.ok || parsed.subcommand !== "add") return null;
|
|
186
|
+
const tail = parsed.args.slice(2);
|
|
187
|
+
if (tail.some((argument) => ["-A", "--all", "-u", "--update", "--refresh", "--renormalize", "--pathspec-from-file"].some((option) => argument === option || argument.startsWith(option + "=")))) return "";
|
|
188
|
+
const separator = tail.indexOf("--");
|
|
189
|
+
const paths = separator >= 0 ? tail.slice(separator + 1) : tail.filter((argument) => !argument.startsWith("-"));
|
|
190
|
+
return paths.join(" ");
|
|
191
|
+
}
|
|
192
|
+
function deriveChecks(commands) {
|
|
193
|
+
const parsedCommands = commands.map(parseCommand).filter((parsed) => parsed.ok);
|
|
194
|
+
const hasCommit = parsedCommands.some((parsed) => parsed.subcommand === "commit");
|
|
195
|
+
const checks = [];
|
|
196
|
+
let lastBranch = null;
|
|
197
|
+
let lastCommitMessage = null;
|
|
198
|
+
const branchesGone = [];
|
|
199
|
+
let lastStaged = [];
|
|
200
|
+
let lastClean = [];
|
|
201
|
+
let stashOperation = null;
|
|
202
|
+
let hasPush = false;
|
|
203
|
+
for (const parsed of parsedCommands) {
|
|
204
|
+
const tail = parsed.args.slice(2);
|
|
205
|
+
if (parsed.subcommand === "switch" || parsed.subcommand === "checkout") {
|
|
206
|
+
const createOptions = parsed.subcommand === "switch" ? ["-c", "-C", "--create", "--force-create"] : ["-b", "-B"];
|
|
207
|
+
for (let index = 0; index < tail.length - 1; index += 1) if (createOptions.includes(tail[index])) lastBranch = tail[index + 1];
|
|
208
|
+
}
|
|
209
|
+
if (parsed.subcommand === "commit") {
|
|
210
|
+
for (let index = 0; index < tail.length; index += 1) {
|
|
211
|
+
const argument = tail[index];
|
|
212
|
+
if ((argument === "-m" || argument === "--message") && index + 1 < tail.length) {
|
|
213
|
+
lastCommitMessage = tail[index + 1];
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
if (argument.startsWith("--message=")) {
|
|
217
|
+
lastCommitMessage = argument.slice("--message=".length);
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (parsed.subcommand === "branch") {
|
|
223
|
+
const deleteAt = tail.findIndex((argument) => argument === "-d" || argument === "-D" || argument === "--delete");
|
|
224
|
+
if (deleteAt >= 0) branchesGone.push(...tail.slice(deleteAt + 1).filter((argument) => !argument.startsWith("-")));
|
|
225
|
+
}
|
|
226
|
+
if (!hasCommit && parsed.subcommand === "add") {
|
|
227
|
+
const paths = addPathsOf(parsed.args.map(quoteShellArg).join(" "));
|
|
228
|
+
const separator = tail.indexOf("--");
|
|
229
|
+
if (paths) lastStaged = separator >= 0 ? tail.slice(separator + 1) : tail.filter((argument) => !argument.startsWith("-"));
|
|
230
|
+
}
|
|
231
|
+
if (parsed.subcommand === "checkout" || parsed.subcommand === "restore") {
|
|
232
|
+
const separator = tail.indexOf("--");
|
|
233
|
+
if (separator >= 0) lastClean = tail.slice(separator + 1);
|
|
234
|
+
else if (parsed.subcommand === "restore" && !tail.some((argument) => argument === "--staged" || argument.startsWith("--staged="))) lastClean = tail.filter((argument) => !argument.startsWith("-"));
|
|
235
|
+
}
|
|
236
|
+
if (parsed.subcommand === "stash" && (tail[0] === "push" || tail.length === 0)) stashOperation = "nonempty";
|
|
237
|
+
if (parsed.subcommand === "stash" && (tail[0] === "drop" || tail[0] === "clear")) stashOperation = "empty";
|
|
238
|
+
if (parsed.subcommand === "push") hasPush = true;
|
|
239
|
+
}
|
|
240
|
+
if (lastBranch) checks.push({ type: "branch", value: lastBranch, label: "\u5F53\u524D\u5206\u652F\u5E94\u4E3A " + lastBranch });
|
|
241
|
+
if (lastCommitMessage !== null) checks.push({ type: "commit-msg", value: lastCommitMessage, label: "\u6700\u8FD1\u63D0\u4EA4\u4FE1\u606F\u5E94\u4E3A\u300C" + lastCommitMessage + "\u300D" });
|
|
242
|
+
for (const branch of branchesGone) checks.push({ type: "branch-gone", value: branch, label: "\u5206\u652F " + branch + " \u5E94\u5DF2\u5220\u9664" });
|
|
243
|
+
if (lastStaged.length) checks.push({ type: "staged", value: lastStaged, label: "\u6307\u5B9A\u6587\u4EF6\u5E94\u5DF2\u6682\u5B58" });
|
|
244
|
+
if (lastClean.length) checks.push({ type: "clean", value: lastClean, label: "\u6307\u5B9A\u6587\u4EF6\u7684\u5DE5\u4F5C\u533A\u6539\u52A8\u5E94\u5DF2\u4E22\u5F03" });
|
|
245
|
+
if (stashOperation === "nonempty") checks.push({ type: "stash-nonempty", value: true, label: "stash \u5E94\u975E\u7A7A" });
|
|
246
|
+
if (stashOperation === "empty") checks.push({ type: "stash-empty", value: true, label: "stash \u5E94\u4E3A\u7A7A" });
|
|
247
|
+
if (hasPush) checks.push({ type: "no-ahead", value: true, label: "\u5E94\u5DF2\u63A8\u9001\uFF08\u4E0D\u518D\u9886\u5148\u8FDC\u7A0B\uFF09" });
|
|
248
|
+
return checks;
|
|
249
|
+
}
|
|
250
|
+
var MAX_COMMAND_LENGTH, allowedSubcommands, safeSubcommands;
|
|
251
|
+
var init_command_policy = __esm({
|
|
252
|
+
"src/host/command-policy.ts"() {
|
|
253
|
+
"use strict";
|
|
254
|
+
MAX_COMMAND_LENGTH = 800;
|
|
255
|
+
allowedSubcommands = /* @__PURE__ */ new Set([
|
|
256
|
+
"add",
|
|
257
|
+
"blame",
|
|
258
|
+
"branch",
|
|
259
|
+
"cat-file",
|
|
260
|
+
"check-attr",
|
|
261
|
+
"check-ignore",
|
|
262
|
+
"checkout",
|
|
263
|
+
"cherry",
|
|
264
|
+
"cherry-pick",
|
|
265
|
+
"clean",
|
|
266
|
+
"commit",
|
|
267
|
+
"count-objects",
|
|
268
|
+
"describe",
|
|
269
|
+
"diff",
|
|
270
|
+
"fetch",
|
|
271
|
+
"for-each-ref",
|
|
272
|
+
"fsck",
|
|
273
|
+
"grep",
|
|
274
|
+
"hash-object",
|
|
275
|
+
"log",
|
|
276
|
+
"ls-files",
|
|
277
|
+
"ls-tree",
|
|
278
|
+
"merge",
|
|
279
|
+
"merge-base",
|
|
280
|
+
"mv",
|
|
281
|
+
"pull",
|
|
282
|
+
"push",
|
|
283
|
+
"rebase",
|
|
284
|
+
"reflog",
|
|
285
|
+
"remote",
|
|
286
|
+
"reset",
|
|
287
|
+
"restore",
|
|
288
|
+
"revert",
|
|
289
|
+
"rev-parse",
|
|
290
|
+
"rm",
|
|
291
|
+
"shortlog",
|
|
292
|
+
"show",
|
|
293
|
+
"show-ref",
|
|
294
|
+
"stash",
|
|
295
|
+
"status",
|
|
296
|
+
"switch",
|
|
297
|
+
"tag",
|
|
298
|
+
"verify-commit",
|
|
299
|
+
"verify-tag",
|
|
300
|
+
"whatchanged"
|
|
301
|
+
]);
|
|
302
|
+
safeSubcommands = /* @__PURE__ */ new Set([
|
|
303
|
+
"blame",
|
|
304
|
+
"check-attr",
|
|
305
|
+
"check-ignore",
|
|
306
|
+
"cherry",
|
|
307
|
+
"count-objects",
|
|
308
|
+
"describe",
|
|
309
|
+
"diff",
|
|
310
|
+
"for-each-ref",
|
|
311
|
+
"grep",
|
|
312
|
+
"log",
|
|
313
|
+
"ls-files",
|
|
314
|
+
"ls-tree",
|
|
315
|
+
"merge-base",
|
|
316
|
+
"rev-parse",
|
|
317
|
+
"shortlog",
|
|
318
|
+
"show",
|
|
319
|
+
"show-ref",
|
|
320
|
+
"status",
|
|
321
|
+
"verify-commit",
|
|
322
|
+
"verify-tag",
|
|
323
|
+
"whatchanged"
|
|
324
|
+
]);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// src/host/actions.ts
|
|
329
|
+
function isRepositoryAction(action) {
|
|
330
|
+
return REPOSITORY_ACTIONS.includes(action);
|
|
331
|
+
}
|
|
332
|
+
function errorMessage(error) {
|
|
333
|
+
return error instanceof Error ? error.message : String(error);
|
|
334
|
+
}
|
|
335
|
+
function asRecord(value) {
|
|
336
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
337
|
+
}
|
|
338
|
+
function readBody(req) {
|
|
339
|
+
return new Promise((resolve) => {
|
|
340
|
+
const chunks = [];
|
|
341
|
+
let size = 0;
|
|
342
|
+
let settled = false;
|
|
343
|
+
const finish = (value) => {
|
|
344
|
+
if (settled) return;
|
|
345
|
+
settled = true;
|
|
346
|
+
resolve(value);
|
|
347
|
+
};
|
|
348
|
+
req.on("data", (chunk) => {
|
|
349
|
+
const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
350
|
+
size += part.length;
|
|
351
|
+
if (size > 1024 * 1024) {
|
|
352
|
+
chunks.length = 0;
|
|
353
|
+
finish(null);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (!settled) chunks.push(part);
|
|
357
|
+
});
|
|
358
|
+
req.on("end", () => finish(Buffer.concat(chunks).toString("utf8")));
|
|
359
|
+
req.on("error", () => finish(""));
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
function sendJson(res, status, data) {
|
|
363
|
+
res.writeHead(status, {
|
|
364
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
365
|
+
"Cache-Control": "no-store",
|
|
366
|
+
"X-Content-Type-Options": "nosniff"
|
|
367
|
+
});
|
|
368
|
+
res.end(JSON.stringify(data));
|
|
369
|
+
}
|
|
370
|
+
async function dispatchRepositoryAction(action, sessionId, body, dependencies) {
|
|
371
|
+
const context = dependencies.repositoryContext(sessionId);
|
|
372
|
+
if (!context) return { ok: false, code: "SESSION_NOT_FOUND", message: "\u65E0\u6CD5\u786E\u5B9A\u5F53\u524D\u4F1A\u8BDD\u7684\u4ED3\u5E93\u76EE\u5F55" };
|
|
373
|
+
const repository = dependencies.repository;
|
|
374
|
+
const base = { sessionId, workdir: context.workdir, operationId: body.operationId, sandboxPolicy: context.policy };
|
|
375
|
+
if (action === "get-summary") return repository.getSummary(context.workdir, void 0, context.policy);
|
|
376
|
+
if (action === "get-diff") return repository.getDiff(context.workdir, body.path, body.staged === true, void 0, context.policy);
|
|
377
|
+
if (action === "get-branches") return repository.getBranches(context.workdir, void 0, context.policy);
|
|
378
|
+
if (action === "get-commits") return repository.getCommits(context.workdir, body.limit, void 0, context.policy);
|
|
379
|
+
if (action === "get-commit-detail") return repository.getCommitDetail(context.workdir, body.hash, void 0, context.policy);
|
|
380
|
+
if (action === "get-commit-diff") return repository.getCommitDiff(context.workdir, body.hash, void 0, context.policy);
|
|
381
|
+
if (action === "get-stashes") return repository.getStashes(context.workdir, void 0, context.policy);
|
|
382
|
+
if (action === "stage-paths") return repository.stagePaths(base, body.paths);
|
|
383
|
+
if (action === "unstage-paths") return repository.unstagePaths(base, body.paths);
|
|
384
|
+
if (action === "stage-all") return repository.stageAll(base);
|
|
385
|
+
if (action === "unstage-all") return repository.unstageAll(base);
|
|
386
|
+
if (action === "commit") return repository.commit(base, body.message);
|
|
387
|
+
if (action === "create-branch") return repository.createBranch(base, body.name, body.base);
|
|
388
|
+
if (action === "switch-branch") return repository.switchBranch(base, body.name);
|
|
389
|
+
return repository.deleteBranch(base, body.name, body.force === true, body.confirmRisk === true);
|
|
390
|
+
}
|
|
391
|
+
async function dispatchProposalAction(action, sessionId, body, dependencies) {
|
|
392
|
+
if (action === "state") {
|
|
393
|
+
const proposal = dependencies.latestPending(sessionId);
|
|
394
|
+
if (proposal && proposal.status === "pending" && proposal.copied === true && proposal.fingerprint) {
|
|
395
|
+
const verification = await dependencies.verifyProposal(dependencies.shell, proposal);
|
|
396
|
+
if (verification.verified) {
|
|
397
|
+
proposal.verified = true;
|
|
398
|
+
proposal.status = "verified";
|
|
399
|
+
await dependencies.flushProposal(sessionId);
|
|
400
|
+
}
|
|
401
|
+
return { status: 200, data: { ok: true, proposal: dependencies.proposalView(proposal), ...verification } };
|
|
402
|
+
}
|
|
403
|
+
return { status: 200, data: { ok: true, proposal: proposal ? dependencies.proposalView(proposal) : null, changed: false, verified: false, partial: false, message: "", changedState: "" } };
|
|
404
|
+
}
|
|
405
|
+
if (action === "dismiss") {
|
|
406
|
+
const proposal = dependencies.findProposal(sessionId, body.proposalId);
|
|
407
|
+
if (!proposal) return { status: 200, data: { ok: false, error: "\u627E\u4E0D\u5230\u8BE5\u63D0\u8BAE" } };
|
|
408
|
+
if (proposal.status === "running") return { status: 200, data: { ok: false, error: "\u8BE5\u63D0\u8BAE\u6B63\u5728\u6267\u884C\uFF0C\u4E0D\u80FD\u653E\u5F03" } };
|
|
409
|
+
proposal.closed = true;
|
|
410
|
+
proposal.status = "dismissed";
|
|
411
|
+
proposal.manual = body.manual === true;
|
|
412
|
+
await dependencies.flushProposal(sessionId);
|
|
413
|
+
return { status: 200, data: { ok: true } };
|
|
414
|
+
}
|
|
415
|
+
if (action === "mark-copied") {
|
|
416
|
+
const proposal = dependencies.findProposal(sessionId, body.proposalId);
|
|
417
|
+
if (!proposal) return { status: 200, data: { ok: false, error: "\u627E\u4E0D\u5230\u8BE5\u63D0\u8BAE" } };
|
|
418
|
+
if (proposal.status !== "pending") return { status: 200, data: { ok: false, error: "\u8BE5\u63D0\u8BAE\u5DF2\u4E0D\u518D\u7B49\u5F85\u6267\u884C" } };
|
|
419
|
+
if (proposal.risk === "hard" && body.confirm !== true) return { status: 200, data: { ok: false, error: "\u9AD8\u98CE\u9669\u64CD\u4F5C\uFF1A\u8BF7\u5148\u52FE\u9009\u201C\u6211\u5DF2\u4E86\u89E3\u98CE\u9669\u201D\u518D\u590D\u5236" } };
|
|
420
|
+
proposal.fingerprint = await dependencies.captureFingerprint(dependencies.shell, proposal.workdir);
|
|
421
|
+
proposal.baselineFailed = await dependencies.runChecks(dependencies.shell, proposal.workdir, deriveChecks(proposal.steps.map((step) => step.command)));
|
|
422
|
+
proposal.copied = true;
|
|
423
|
+
await dependencies.flushProposal(sessionId);
|
|
424
|
+
return { status: 200, data: { ok: true } };
|
|
425
|
+
}
|
|
426
|
+
if (action === "verify") {
|
|
427
|
+
const proposal = dependencies.findProposal(sessionId, body.proposalId);
|
|
428
|
+
if (!proposal) return { status: 200, data: { ok: false, error: "\u627E\u4E0D\u5230\u8BE5\u63D0\u8BAE" } };
|
|
429
|
+
if (proposal.status !== "pending") return { status: 200, data: { ok: false, changed: false, verified: false, partial: false, message: "\u8BE5\u63D0\u8BAE\u5DF2\u4E0D\u518D\u7B49\u5F85\u624B\u52A8\u9A8C\u8BC1", changedState: "" } };
|
|
430
|
+
if (proposal.copied !== true || !proposal.fingerprint) {
|
|
431
|
+
return { status: 200, data: { ok: true, changed: false, verified: false, partial: false, message: "\u5C1A\u672A\u590D\u5236\u547D\u4EE4\u6216\u7F3A\u5C11\u5BF9\u6BD4\u57FA\u7EBF", changedState: "" } };
|
|
432
|
+
}
|
|
433
|
+
const verification = await dependencies.verifyProposal(dependencies.shell, proposal);
|
|
434
|
+
if (verification.verified) {
|
|
435
|
+
proposal.verified = true;
|
|
436
|
+
proposal.status = "verified";
|
|
437
|
+
}
|
|
438
|
+
await dependencies.flushProposal(sessionId);
|
|
439
|
+
return { status: 200, data: { ok: true, ...verification } };
|
|
440
|
+
}
|
|
441
|
+
if (action === "execute") {
|
|
442
|
+
const proposal = dependencies.findProposal(sessionId, body.proposalId);
|
|
443
|
+
if (!proposal) {
|
|
444
|
+
return { status: 200, data: { ok: false, proposalId: body.proposalId || "", command: "", steps: [], exitCode: -1, signal: "", timedOut: false, stdout: "", stderr: "", diagnostics: "", error: "\u627E\u4E0D\u5230\u8BE5\u63D0\u8BAE\uFF08proposalId \u65E0\u6548\u6216\u5DF2\u8FC7\u671F\uFF09" } };
|
|
445
|
+
}
|
|
446
|
+
if (proposal.risk === "hard" && body.confirm !== true) {
|
|
447
|
+
return { status: 200, data: { ok: false, proposalId: proposal.proposalId, command: proposal.command, steps: [], exitCode: -1, signal: "", timedOut: false, stdout: "", stderr: "", diagnostics: "", error: "\u9AD8\u98CE\u9669\u64CD\u4F5C\uFF1A\u8BF7\u5148\u52FE\u9009\u201C\u6211\u5DF2\u4E86\u89E3\u98CE\u9669\u201D\u518D\u6267\u884C" } };
|
|
448
|
+
}
|
|
449
|
+
const result = await dependencies.executeProposal(
|
|
450
|
+
dependencies.shell,
|
|
451
|
+
proposal,
|
|
452
|
+
dependencies.resolveExecutionPolicy(sessionId),
|
|
453
|
+
() => dependencies.flushProposal(sessionId)
|
|
454
|
+
);
|
|
455
|
+
console.log("git-guide HTTP execute", proposal.proposalId, "ok=", result.ok);
|
|
456
|
+
return { status: 200, data: result };
|
|
457
|
+
}
|
|
458
|
+
return { status: 400, data: { ok: false, error: "unknown action: " + action } };
|
|
459
|
+
}
|
|
460
|
+
function registerGitGuideActions(webServer, dependencies) {
|
|
461
|
+
if (!webServer) return void 0;
|
|
462
|
+
return webServer.register({
|
|
463
|
+
kind: "prefix",
|
|
464
|
+
path: "/git-guide",
|
|
465
|
+
handler: async (req, res) => {
|
|
466
|
+
if (req.method !== "POST") {
|
|
467
|
+
sendJson(res, 405, { ok: false, error: "method not allowed" });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (req.headers?.["sec-fetch-site"] === "cross-site") {
|
|
471
|
+
sendJson(res, 403, { ok: false, error: "cross-site request denied" });
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const contentType = req.headers?.["content-type"];
|
|
475
|
+
if (typeof contentType !== "string" || !/^application\/json(?:\s*;|$)/i.test(contentType)) {
|
|
476
|
+
sendJson(res, 415, { ok: false, error: "content-type must be application/json" });
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
let body;
|
|
480
|
+
try {
|
|
481
|
+
const raw = await readBody(req);
|
|
482
|
+
if (raw === null) {
|
|
483
|
+
sendJson(res, 413, { ok: false, error: "request body too large" });
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
body = raw ? asRecord(JSON.parse(raw)) : {};
|
|
487
|
+
} catch (error) {
|
|
488
|
+
sendJson(res, 400, { ok: false, error: "invalid json body" });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
|
|
492
|
+
if (!sessionId || sessionId.length > 200) {
|
|
493
|
+
sendJson(res, 400, { ok: false, error: "valid sessionId is required" });
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const action = typeof body.action === "string" ? body.action : "";
|
|
497
|
+
try {
|
|
498
|
+
await dependencies.proposalStorageReady;
|
|
499
|
+
if (isRepositoryAction(action)) {
|
|
500
|
+
sendJson(res, 200, await dispatchRepositoryAction(action, sessionId, body, dependencies));
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const response = await dispatchProposalAction(action, sessionId, body, dependencies);
|
|
504
|
+
sendJson(res, response.status, response.data);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
const diagnostics = redactAndLimit(errorMessage(error), 2e3);
|
|
507
|
+
sendJson(res, 500, isRepositoryAction(action) ? { ok: false, code: "INTERNAL_ERROR", message: "Git \u64CD\u4F5C\u5931\u8D25", diagnostics } : { ok: false, error: diagnostics });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
var REPOSITORY_ACTIONS;
|
|
513
|
+
var init_actions = __esm({
|
|
514
|
+
"src/host/actions.ts"() {
|
|
515
|
+
"use strict";
|
|
516
|
+
init_command_policy();
|
|
517
|
+
REPOSITORY_ACTIONS = [
|
|
518
|
+
"get-summary",
|
|
519
|
+
"get-diff",
|
|
520
|
+
"get-branches",
|
|
521
|
+
"get-commits",
|
|
522
|
+
"get-commit-detail",
|
|
523
|
+
"get-commit-diff",
|
|
524
|
+
"get-stashes",
|
|
525
|
+
"stage-paths",
|
|
526
|
+
"unstage-paths",
|
|
527
|
+
"stage-all",
|
|
528
|
+
"unstage-all",
|
|
529
|
+
"commit",
|
|
530
|
+
"create-branch",
|
|
531
|
+
"switch-branch",
|
|
532
|
+
"delete-branch"
|
|
533
|
+
];
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// src/host/proposal-service.ts
|
|
538
|
+
function isRecord(value) {
|
|
539
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
540
|
+
}
|
|
541
|
+
function isStoredProposal(value, sessionId) {
|
|
542
|
+
if (!isRecord(value) || value.sessionId !== sessionId) return false;
|
|
543
|
+
if (typeof value.proposalId !== "string" || !/^g-[0-9a-f-]{36}$/i.test(value.proposalId)) return false;
|
|
544
|
+
if (typeof value.intent !== "string" || typeof value.command !== "string" || typeof value.explanation !== "string") return false;
|
|
545
|
+
if (typeof value.workdir !== "string" || typeof value.createdAt !== "number" || !Number.isSafeInteger(value.createdAt)) return false;
|
|
546
|
+
if (!["safe", "normal", "hard"].includes(String(value.risk))) return false;
|
|
547
|
+
if (!["pending", "running", "succeeded", "failed", "verified", "dismissed"].includes(String(value.status))) return false;
|
|
548
|
+
if (!Array.isArray(value.reasons) || !value.reasons.every((reason) => typeof reason === "string")) return false;
|
|
549
|
+
if (!Array.isArray(value.steps) || value.steps.length === 0 || value.steps.length > 10) return false;
|
|
550
|
+
if (!value.steps.every((step) => isRecord(step) && typeof step.command === "string" && (step.result === null || isRecord(step.result)))) return false;
|
|
551
|
+
return typeof value.confirmed === "boolean" && typeof value.closed === "boolean" && typeof value.copied === "boolean" && typeof value.verified === "boolean" && (value.fingerprint === null || typeof value.fingerprint === "string") && (value.result === null || isRecord(value.result));
|
|
552
|
+
}
|
|
553
|
+
var import_node_crypto, DEFAULT_PROPOSALS_PER_SESSION, DEFAULT_MAX_SESSIONS, DEFAULT_SESSION_TTL_MS, ProposalService, proposalService;
|
|
554
|
+
var init_proposal_service = __esm({
|
|
555
|
+
"src/host/proposal-service.ts"() {
|
|
556
|
+
"use strict";
|
|
557
|
+
import_node_crypto = require("node:crypto");
|
|
558
|
+
DEFAULT_PROPOSALS_PER_SESSION = 3;
|
|
559
|
+
DEFAULT_MAX_SESSIONS = 100;
|
|
560
|
+
DEFAULT_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
561
|
+
ProposalService = class {
|
|
562
|
+
constructor(proposalsPerSession = DEFAULT_PROPOSALS_PER_SESSION, maxSessions = DEFAULT_MAX_SESSIONS, sessionTtlMs = DEFAULT_SESSION_TTL_MS) {
|
|
563
|
+
this.proposalsPerSession = proposalsPerSession;
|
|
564
|
+
this.maxSessions = maxSessions;
|
|
565
|
+
this.sessionTtlMs = sessionTtlMs;
|
|
566
|
+
}
|
|
567
|
+
proposalsBySession = /* @__PURE__ */ new Map();
|
|
568
|
+
storage = null;
|
|
569
|
+
storageTail = Promise.resolve();
|
|
570
|
+
async attachStorage(storage) {
|
|
571
|
+
const snapshot = await storage.loadAll();
|
|
572
|
+
const records = snapshot.tables.proposals ?? {};
|
|
573
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
574
|
+
for (const [sessionId, value] of Object.entries(records)) {
|
|
575
|
+
const entries = this.readStoredEntries(sessionId, value);
|
|
576
|
+
if (entries.length > 0) loaded.set(sessionId, entries);
|
|
577
|
+
}
|
|
578
|
+
this.pruneMap(loaded);
|
|
579
|
+
for (const [sessionId, entries] of this.proposalsBySession) {
|
|
580
|
+
if (!loaded.has(sessionId)) loaded.set(sessionId, entries);
|
|
581
|
+
}
|
|
582
|
+
this.proposalsBySession.clear();
|
|
583
|
+
for (const [sessionId, entries] of loaded) this.proposalsBySession.set(sessionId, entries);
|
|
584
|
+
this.storage = storage;
|
|
585
|
+
}
|
|
586
|
+
async closeStorage(storage) {
|
|
587
|
+
await this.storageTail;
|
|
588
|
+
if (this.storage === storage) this.storage = null;
|
|
589
|
+
await storage.close();
|
|
590
|
+
}
|
|
591
|
+
flush(sessionId) {
|
|
592
|
+
const storage = this.storage;
|
|
593
|
+
if (!storage) return Promise.resolve();
|
|
594
|
+
const entries = this.proposalsBySession.get(sessionId);
|
|
595
|
+
const operation = entries && entries.length > 0 ? () => storage.putRecord("proposals", sessionId, { entries }) : () => storage.deleteRecord("proposals", sessionId);
|
|
596
|
+
const result = this.storageTail.then(operation);
|
|
597
|
+
this.storageTail = result.catch(() => {
|
|
598
|
+
});
|
|
599
|
+
return result;
|
|
600
|
+
}
|
|
601
|
+
newId() {
|
|
602
|
+
return "g-" + (0, import_node_crypto.randomUUID)();
|
|
603
|
+
}
|
|
604
|
+
list(sessionId) {
|
|
605
|
+
this.prune();
|
|
606
|
+
return this.proposalsBySession.get(sessionId);
|
|
607
|
+
}
|
|
608
|
+
hasRunning(sessionId) {
|
|
609
|
+
return this.list(sessionId)?.some((proposal) => proposal.status === "running") ?? false;
|
|
610
|
+
}
|
|
611
|
+
closeOpen(sessionId) {
|
|
612
|
+
for (const proposal of this.list(sessionId) ?? []) if (!proposal.closed) proposal.closed = true;
|
|
613
|
+
}
|
|
614
|
+
store(sessionId, proposal) {
|
|
615
|
+
this.prune();
|
|
616
|
+
let list = this.proposalsBySession.get(sessionId);
|
|
617
|
+
if (!list) {
|
|
618
|
+
list = [];
|
|
619
|
+
this.proposalsBySession.set(sessionId, list);
|
|
620
|
+
} else {
|
|
621
|
+
this.proposalsBySession.delete(sessionId);
|
|
622
|
+
this.proposalsBySession.set(sessionId, list);
|
|
623
|
+
}
|
|
624
|
+
list.unshift(proposal);
|
|
625
|
+
if (list.length > this.proposalsPerSession) list.length = this.proposalsPerSession;
|
|
626
|
+
while (this.proposalsBySession.size > this.maxSessions) {
|
|
627
|
+
const oldest = [...this.proposalsBySession].find(([, entries]) => entries.every((entry) => entry.status !== "running"))?.[0];
|
|
628
|
+
if (!oldest) break;
|
|
629
|
+
this.proposalsBySession.delete(oldest);
|
|
630
|
+
}
|
|
631
|
+
return proposal;
|
|
632
|
+
}
|
|
633
|
+
prune(now = Date.now()) {
|
|
634
|
+
this.pruneMap(this.proposalsBySession, now);
|
|
635
|
+
}
|
|
636
|
+
pruneMap(proposals, now = Date.now()) {
|
|
637
|
+
for (const [sessionId, entries] of proposals) {
|
|
638
|
+
const newest = entries[0];
|
|
639
|
+
if (!newest || now - newest.createdAt > this.sessionTtlMs && entries.every((entry) => entry.status !== "running")) proposals.delete(sessionId);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
readStoredEntries(sessionId, value) {
|
|
643
|
+
if (!isRecord(value) || !Array.isArray(value.entries)) return [];
|
|
644
|
+
const entries = [];
|
|
645
|
+
for (const item of value.entries.slice(0, this.proposalsPerSession)) {
|
|
646
|
+
if (!isStoredProposal(item, sessionId)) continue;
|
|
647
|
+
if (item.status === "running") {
|
|
648
|
+
entries.push({
|
|
649
|
+
...item,
|
|
650
|
+
status: "failed",
|
|
651
|
+
closed: true,
|
|
652
|
+
result: { ok: false, error: "DSH \u5728\u547D\u4EE4\u6267\u884C\u671F\u95F4\u505C\u6B62\uFF0C\u65E0\u6CD5\u786E\u8BA4\u547D\u4EE4\u662F\u5426\u5B8C\u6574\u6267\u884C" }
|
|
653
|
+
});
|
|
654
|
+
} else {
|
|
655
|
+
entries.push(item);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return entries;
|
|
659
|
+
}
|
|
660
|
+
find(sessionId, proposalId) {
|
|
661
|
+
if (typeof proposalId !== "string" || !proposalId) return void 0;
|
|
662
|
+
return this.list(sessionId)?.find((proposal) => proposal.proposalId === proposalId);
|
|
663
|
+
}
|
|
664
|
+
latestPending(sessionId) {
|
|
665
|
+
for (const proposal of this.list(sessionId) ?? []) if (!proposal.closed) return proposal;
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
view(proposal) {
|
|
669
|
+
return {
|
|
670
|
+
proposalId: proposal.proposalId,
|
|
671
|
+
intent: proposal.intent,
|
|
672
|
+
command: proposal.command,
|
|
673
|
+
steps: proposal.steps.map((step) => ({ command: step.command, result: step.result })),
|
|
674
|
+
explanation: proposal.explanation,
|
|
675
|
+
risk: proposal.risk,
|
|
676
|
+
reasons: proposal.reasons,
|
|
677
|
+
confirmed: proposal.confirmed,
|
|
678
|
+
workdir: proposal.workdir,
|
|
679
|
+
result: proposal.result,
|
|
680
|
+
closed: proposal.closed,
|
|
681
|
+
copied: proposal.copied,
|
|
682
|
+
status: proposal.status || (proposal.closed ? "dismissed" : "pending")
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
proposalService = new ProposalService();
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// src/host/git-repository-service.ts
|
|
691
|
+
function errorResult(code, message, diagnostics, reason) {
|
|
692
|
+
return {
|
|
693
|
+
ok: false,
|
|
694
|
+
code,
|
|
695
|
+
message,
|
|
696
|
+
...diagnostics ? { diagnostics } : {},
|
|
697
|
+
...reason ? { reason } : {}
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function outputOf(result) {
|
|
701
|
+
return ((result.stdout?.text ?? "") + (result.stderr?.text ?? "")).trim();
|
|
702
|
+
}
|
|
703
|
+
function mutationErrorCode(result) {
|
|
704
|
+
if (result.timedOut) return "TIMEOUT";
|
|
705
|
+
return "GIT_FAILED";
|
|
706
|
+
}
|
|
707
|
+
function parseStatus(status) {
|
|
708
|
+
const files = [];
|
|
709
|
+
for (const line of status.split("\n")) {
|
|
710
|
+
if (!line || line.startsWith("## ") || line.length < 4) continue;
|
|
711
|
+
const indexStatus = line[0] ?? " ";
|
|
712
|
+
const workTreeStatus = line[1] ?? " ";
|
|
713
|
+
const rawPath = line.slice(3);
|
|
714
|
+
const renameAt = rawPath.indexOf(" -> ");
|
|
715
|
+
files.push(renameAt >= 0 ? { indexStatus, workTreeStatus, path: rawPath.slice(renameAt + 4), originalPath: rawPath.slice(0, renameAt) } : { indexStatus, workTreeStatus, path: rawPath });
|
|
716
|
+
}
|
|
717
|
+
return files;
|
|
718
|
+
}
|
|
719
|
+
function validPathspec(path) {
|
|
720
|
+
if (typeof path !== "string" || !path || path.length > 4096 || /[\0\r\n]/.test(path)) return false;
|
|
721
|
+
if (path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path)) return false;
|
|
722
|
+
return !path.split(/[\\/]/).some((part) => part === "..");
|
|
723
|
+
}
|
|
724
|
+
function validBranchName(name) {
|
|
725
|
+
return typeof name === "string" && name.length > 0 && name.length <= 255 && !/[\0\r\n\s~^:?*\[\\]/.test(name) && !name.startsWith("-") && !name.startsWith(".") && !name.endsWith(".") && !name.includes("..") && !name.includes("@{") && !name.endsWith(".lock");
|
|
726
|
+
}
|
|
727
|
+
function validCommitHash(hash) {
|
|
728
|
+
return typeof hash === "string" && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(hash);
|
|
729
|
+
}
|
|
730
|
+
function parseCommitFiles(nameStatus, numstat) {
|
|
731
|
+
const statusRows = nameStatus.split("\n").filter(Boolean);
|
|
732
|
+
const statRows = numstat.split("\n").filter(Boolean);
|
|
733
|
+
let additions = 0;
|
|
734
|
+
let deletions = 0;
|
|
735
|
+
let binary = 0;
|
|
736
|
+
const files = statusRows.slice(0, COMMIT_FILE_MAX).map((line, index) => {
|
|
737
|
+
const parts = line.split(" ");
|
|
738
|
+
const status = parts[0] ?? "";
|
|
739
|
+
const renamed = /^[RC]/.test(status) && parts.length >= 3;
|
|
740
|
+
const path = redactAndLimit(renamed ? parts[2] ?? "" : parts[1] ?? "", 4096);
|
|
741
|
+
const previousPath = renamed ? redactAndLimit(parts[1] ?? "", 4096) : void 0;
|
|
742
|
+
const stat = (statRows[index] ?? "").split(" ");
|
|
743
|
+
const added = /^\d+$/.test(stat[0] ?? "") ? Number(stat[0]) : null;
|
|
744
|
+
const deleted = /^\d+$/.test(stat[1] ?? "") ? Number(stat[1]) : null;
|
|
745
|
+
if (added === null || deleted === null) binary += 1;
|
|
746
|
+
else {
|
|
747
|
+
additions += added;
|
|
748
|
+
deletions += deleted;
|
|
749
|
+
}
|
|
750
|
+
return { status: redactAndLimit(status, 32), path, ...previousPath ? { previousPath } : {}, additions: added, deletions: deleted };
|
|
751
|
+
});
|
|
752
|
+
for (const line of statRows.slice(files.length)) {
|
|
753
|
+
const stat = line.split(" ");
|
|
754
|
+
const added = /^\d+$/.test(stat[0] ?? "") ? Number(stat[0]) : null;
|
|
755
|
+
const deleted = /^\d+$/.test(stat[1] ?? "") ? Number(stat[1]) : null;
|
|
756
|
+
if (added === null || deleted === null) binary += 1;
|
|
757
|
+
else {
|
|
758
|
+
additions += added;
|
|
759
|
+
deletions += deleted;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return {
|
|
763
|
+
files,
|
|
764
|
+
filesTruncated: statusRows.length > COMMIT_FILE_MAX,
|
|
765
|
+
totals: { files: statusRows.length, additions, deletions, binary }
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
var MUTATION_OUTPUT_MAX_CHARS, DIFF_MAX_CHARS, COMMIT_DETAIL_MAX_CHARS, COMMIT_DIFF_MAX_CHARS, COMMIT_FILE_MAX, STASH_MAX, MAX_PATHS, OPERATION_TTL_MS, GitRepositoryService;
|
|
769
|
+
var init_git_repository_service = __esm({
|
|
770
|
+
"src/host/git-repository-service.ts"() {
|
|
771
|
+
"use strict";
|
|
772
|
+
init_command_policy();
|
|
773
|
+
MUTATION_OUTPUT_MAX_CHARS = 1e5;
|
|
774
|
+
DIFF_MAX_CHARS = 2e5;
|
|
775
|
+
COMMIT_DETAIL_MAX_CHARS = 1e5;
|
|
776
|
+
COMMIT_DIFF_MAX_CHARS = 3e5;
|
|
777
|
+
COMMIT_FILE_MAX = 500;
|
|
778
|
+
STASH_MAX = 100;
|
|
779
|
+
MAX_PATHS = 100;
|
|
780
|
+
OPERATION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
781
|
+
GitRepositoryService = class {
|
|
782
|
+
constructor(shell) {
|
|
783
|
+
this.shell = shell;
|
|
784
|
+
}
|
|
785
|
+
locks = /* @__PURE__ */ new Map();
|
|
786
|
+
operations = /* @__PURE__ */ new Map();
|
|
787
|
+
async run(workdir, command, timeoutMs = 2e4, stdoutMaxBytes = 3e4, signal, sandboxPolicy) {
|
|
788
|
+
if (!this.shell) return { exitCode: -1, stderr: { text: "shell \u670D\u52A1\u4E0D\u53EF\u7528" } };
|
|
789
|
+
try {
|
|
790
|
+
const specification = this.shell.resolve({ command, workdir, timeoutMs, stdoutMaxBytes, signal, ...sandboxPolicy ? { sandboxPolicy } : {} });
|
|
791
|
+
return await this.shell.run(specification);
|
|
792
|
+
} catch (error) {
|
|
793
|
+
return { exitCode: -1, stderr: { text: error instanceof Error ? error.message : String(error) } };
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
async getTopLevel(workdir, signal, sandboxPolicy) {
|
|
797
|
+
const result = await this.run(workdir, "git rev-parse --show-toplevel", 15e3, 4096, signal, sandboxPolicy);
|
|
798
|
+
const topLevel = redactAndLimit(result.stdout?.text ?? "", 4096).trim();
|
|
799
|
+
if (result.exitCode !== 0 || !topLevel || !/^\/|^[A-Za-z]:[\\/]/.test(topLevel)) {
|
|
800
|
+
return errorResult("NOT_GIT_REPOSITORY", "\u76EE\u6807\u76EE\u5F55\u4E0D\u662F Git \u4ED3\u5E93", redactAndLimit(outputOf(result), 4096));
|
|
801
|
+
}
|
|
802
|
+
return { ok: true, data: { topLevel } };
|
|
803
|
+
}
|
|
804
|
+
async getSummary(workdir, signal, sandboxPolicy) {
|
|
805
|
+
const topLevel = await this.getTopLevel(workdir, signal, sandboxPolicy);
|
|
806
|
+
if (!topLevel.ok) return topLevel;
|
|
807
|
+
const [branchResult, headResult, statusResult] = await Promise.all([
|
|
808
|
+
this.run(workdir, "git branch --show-current", 15e3, 4096, signal, sandboxPolicy),
|
|
809
|
+
this.run(workdir, "git rev-parse --short HEAD", 15e3, 4096, signal, sandboxPolicy),
|
|
810
|
+
this.run(workdir, "git status --porcelain=v1 --branch --untracked-files=all", 15e3, 5e4, signal, sandboxPolicy)
|
|
811
|
+
]);
|
|
812
|
+
if (branchResult.exitCode !== 0 || headResult.exitCode !== 0 || statusResult.exitCode !== 0) {
|
|
813
|
+
return errorResult("GIT_FAILED", "\u65E0\u6CD5\u8BFB\u53D6 Git \u4ED3\u5E93\u6458\u8981", redactAndLimit(outputOf(branchResult) + "\n" + outputOf(headResult) + "\n" + outputOf(statusResult), 8192));
|
|
814
|
+
}
|
|
815
|
+
const status = redactAndLimit(statusResult.stdout?.text ?? "", 5e4);
|
|
816
|
+
const files = parseStatus(status);
|
|
817
|
+
return {
|
|
818
|
+
ok: true,
|
|
819
|
+
data: {
|
|
820
|
+
topLevel: topLevel.data.topLevel,
|
|
821
|
+
branch: redactAndLimit(branchResult.stdout?.text ?? "", 4096).trim(),
|
|
822
|
+
head: redactAndLimit(headResult.stdout?.text ?? "", 4096).trim(),
|
|
823
|
+
status,
|
|
824
|
+
files,
|
|
825
|
+
stagedCount: files.filter((file) => file.indexStatus !== " " && file.indexStatus !== "?").length
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
async getDiff(workdir, path, staged, signal, sandboxPolicy) {
|
|
830
|
+
if (path !== void 0 && path !== null && !validPathspec(path)) return errorResult("INVALID_ARGUMENT", "path \u5FC5\u987B\u662F\u4ED3\u5E93\u5185\u7684\u76F8\u5BF9\u8DEF\u5F84");
|
|
831
|
+
const topLevel = await this.getTopLevel(workdir, signal, sandboxPolicy);
|
|
832
|
+
if (!topLevel.ok) return topLevel;
|
|
833
|
+
const pathArgument = typeof path === "string" ? " -- " + quoteShellArg(path) : "";
|
|
834
|
+
const command = "git diff" + (staged ? " --cached" : "") + pathArgument;
|
|
835
|
+
const result = await this.run(workdir, command, 2e4, DIFF_MAX_CHARS + 1024, signal, sandboxPolicy);
|
|
836
|
+
if (result.exitCode !== 0) return errorResult("GIT_FAILED", "\u65E0\u6CD5\u8BFB\u53D6 Git Diff", redactAndLimit(outputOf(result), 8192));
|
|
837
|
+
let raw = redactSecrets(result.stdout?.text ?? "");
|
|
838
|
+
if (!staged && typeof path === "string" && raw.length === 0) {
|
|
839
|
+
const tracked = await this.run(workdir, "git ls-files --error-unmatch -- " + quoteShellArg(path), 15e3, 4096, signal, sandboxPolicy);
|
|
840
|
+
if (tracked.exitCode !== 0) {
|
|
841
|
+
const untracked = await this.run(workdir, "git diff --no-index -- /dev/null " + quoteShellArg(path), 2e4, DIFF_MAX_CHARS + 1024, signal, sandboxPolicy);
|
|
842
|
+
if (untracked.exitCode !== 0 && untracked.exitCode !== 1) {
|
|
843
|
+
return errorResult("GIT_FAILED", "\u65E0\u6CD5\u8BFB\u53D6\u672A\u8DDF\u8E2A\u6587\u4EF6\u7684 Diff", redactAndLimit(outputOf(untracked), 8192));
|
|
844
|
+
}
|
|
845
|
+
raw = redactSecrets(untracked.stdout?.text ?? "");
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return { ok: true, data: { path: typeof path === "string" ? path : null, staged, diff: raw.slice(0, DIFF_MAX_CHARS), truncated: raw.length > DIFF_MAX_CHARS } };
|
|
849
|
+
}
|
|
850
|
+
async getBranches(workdir, signal, sandboxPolicy) {
|
|
851
|
+
const topLevel = await this.getTopLevel(workdir, signal, sandboxPolicy);
|
|
852
|
+
if (!topLevel.ok) return topLevel;
|
|
853
|
+
const branchFormat = "%(HEAD)%09%(refname:short)%09%(upstream:short)";
|
|
854
|
+
const referenceFormat = "%(refname)%09%(refname:short)%09%(objectname:short)%09%(*objectname:short)%09%(subject)";
|
|
855
|
+
const [branchResult, referenceResult] = await Promise.all([
|
|
856
|
+
this.run(workdir, "git branch --format=" + quoteShellArg(branchFormat), 15e3, 3e4, signal, sandboxPolicy),
|
|
857
|
+
this.run(workdir, "git for-each-ref --format=" + quoteShellArg(referenceFormat) + " refs/remotes refs/tags", 15e3, 8e4, signal, sandboxPolicy)
|
|
858
|
+
]);
|
|
859
|
+
if (branchResult.exitCode !== 0 || referenceResult.exitCode !== 0) {
|
|
860
|
+
return errorResult("GIT_FAILED", "\u65E0\u6CD5\u8BFB\u53D6\u5206\u652F\u548C\u6807\u7B7E", redactAndLimit(outputOf(branchResult) + "\n" + outputOf(referenceResult), 8192));
|
|
861
|
+
}
|
|
862
|
+
const branches = (branchResult.stdout?.text ?? "").split("\n").filter(Boolean).map((line) => {
|
|
863
|
+
const [head = "", name = "", upstream = ""] = line.split(" ");
|
|
864
|
+
return { name: redactAndLimit(name, 512), current: head === "*", upstream: redactAndLimit(upstream, 512) };
|
|
865
|
+
});
|
|
866
|
+
const remotes = [];
|
|
867
|
+
const tags = [];
|
|
868
|
+
for (const line of (referenceResult.stdout?.text ?? "").split("\n").filter(Boolean)) {
|
|
869
|
+
const [fullName = "", shortName = "", objectHash = "", peeledHash = "", subject = ""] = line.split(" ");
|
|
870
|
+
const entry = {
|
|
871
|
+
name: redactAndLimit(shortName, 512),
|
|
872
|
+
hash: redactAndLimit(peeledHash || objectHash, 128),
|
|
873
|
+
subject: redactAndLimit(subject, 4096)
|
|
874
|
+
};
|
|
875
|
+
if (fullName.startsWith("refs/remotes/") && !fullName.endsWith("/HEAD")) remotes.push(entry);
|
|
876
|
+
if (fullName.startsWith("refs/tags/")) tags.push(entry);
|
|
877
|
+
}
|
|
878
|
+
return { ok: true, data: { branches, remotes, tags } };
|
|
879
|
+
}
|
|
880
|
+
async getCommits(workdir, limit, signal, sandboxPolicy) {
|
|
881
|
+
const resolvedLimit = typeof limit === "number" && Number.isInteger(limit) && limit >= 1 && limit <= 100 ? limit : 30;
|
|
882
|
+
const topLevel = await this.getTopLevel(workdir, signal, sandboxPolicy);
|
|
883
|
+
if (!topLevel.ok) return topLevel;
|
|
884
|
+
const logFormat = "%H%x1f%P%x1f%s%x1f%an%x1f%aI";
|
|
885
|
+
const refFormat = "%(objectname)%09%(*objectname)%09%(refname)%09%(HEAD)%09%(upstream:short)";
|
|
886
|
+
const [result, refsResult] = await Promise.all([
|
|
887
|
+
this.run(workdir, "git log --date-order -n " + resolvedLimit + " --format=" + quoteShellArg(logFormat), 15e3, 8e4, signal, sandboxPolicy),
|
|
888
|
+
this.run(workdir, "git for-each-ref --format=" + quoteShellArg(refFormat) + " refs/heads refs/remotes refs/tags", 15e3, 8e4, signal, sandboxPolicy)
|
|
889
|
+
]);
|
|
890
|
+
if (result.exitCode !== 0 || refsResult.exitCode !== 0) {
|
|
891
|
+
return errorResult("GIT_FAILED", "\u65E0\u6CD5\u8BFB\u53D6\u63D0\u4EA4\u8BB0\u5F55", redactAndLimit(outputOf(result) + "\n" + outputOf(refsResult), 8192));
|
|
892
|
+
}
|
|
893
|
+
const refLines = (refsResult.stdout?.text ?? "").split("\n").filter(Boolean).map((line) => line.split(" "));
|
|
894
|
+
const currentRef = refLines.find((parts) => parts[3] === "*");
|
|
895
|
+
const currentUpstream = currentRef?.[4] ?? "";
|
|
896
|
+
const refsByHash = /* @__PURE__ */ new Map();
|
|
897
|
+
for (const parts of refLines) {
|
|
898
|
+
const [objectHash = "", peeledHash = "", fullName = "", head = ""] = parts;
|
|
899
|
+
const hash = peeledHash || objectHash;
|
|
900
|
+
let ref = null;
|
|
901
|
+
if (fullName.startsWith("refs/heads/")) ref = { name: fullName.slice(11), type: "branch", current: head === "*" };
|
|
902
|
+
else if (fullName.startsWith("refs/remotes/") && !fullName.endsWith("/HEAD")) ref = { name: fullName.slice(13), type: "remote", current: false };
|
|
903
|
+
else if (fullName.startsWith("refs/tags/")) ref = { name: fullName.slice(10), type: "tag", current: false };
|
|
904
|
+
const relevant = ref?.type === "tag" || ref?.current === true || ref?.type === "remote" && ref.name === currentUpstream;
|
|
905
|
+
if (!ref || !hash || !relevant) continue;
|
|
906
|
+
const safeRef = { ...ref, name: redactAndLimit(ref.name, 512) };
|
|
907
|
+
refsByHash.set(hash, [...refsByHash.get(hash) ?? [], safeRef]);
|
|
908
|
+
}
|
|
909
|
+
const commits = (result.stdout?.text ?? "").split("\n").filter(Boolean).map((line) => {
|
|
910
|
+
const [hash = "", parents = "", subject = "", author = "", date = ""] = line.split("");
|
|
911
|
+
return {
|
|
912
|
+
hash: redactAndLimit(hash, 128),
|
|
913
|
+
parents: parents.split(" ").filter(Boolean).map((parent) => redactAndLimit(parent, 128)),
|
|
914
|
+
subject: redactAndLimit(subject, 4096),
|
|
915
|
+
author: redactAndLimit(author, 512),
|
|
916
|
+
date: redactAndLimit(date, 128),
|
|
917
|
+
refs: refsByHash.get(hash) ?? []
|
|
918
|
+
};
|
|
919
|
+
});
|
|
920
|
+
return { ok: true, data: commits };
|
|
921
|
+
}
|
|
922
|
+
async getCommitDetail(workdir, hash, signal, sandboxPolicy) {
|
|
923
|
+
if (!validCommitHash(hash)) return errorResult("INVALID_ARGUMENT", "\u63D0\u4EA4\u54C8\u5E0C\u5FC5\u987B\u662F\u5B8C\u6574\u7684 40 \u6216 64 \u4F4D\u5341\u516D\u8FDB\u5236\u5B57\u7B26");
|
|
924
|
+
const topLevel = await this.getTopLevel(workdir, signal, sandboxPolicy);
|
|
925
|
+
if (!topLevel.ok) return topLevel;
|
|
926
|
+
const format = "%H%x1f%P%x1f%an%x1f%ae%x1f%aI%x1f%cn%x1f%ce%x1f%cI%x1f%s%x1f%b%x1e";
|
|
927
|
+
const metadata = await this.run(
|
|
928
|
+
workdir,
|
|
929
|
+
"git show -s --no-show-signature --format=" + quoteShellArg(format) + " " + quoteShellArg(hash),
|
|
930
|
+
15e3,
|
|
931
|
+
COMMIT_DETAIL_MAX_CHARS,
|
|
932
|
+
signal,
|
|
933
|
+
sandboxPolicy
|
|
934
|
+
);
|
|
935
|
+
if (metadata.exitCode !== 0) return errorResult("GIT_FAILED", "\u65E0\u6CD5\u8BFB\u53D6\u63D0\u4EA4\u8BE6\u60C5", redactAndLimit(outputOf(metadata), 8192));
|
|
936
|
+
const fields = (metadata.stdout?.text ?? "").replace(/\x1e\s*$/, "").split("");
|
|
937
|
+
const resolvedHash = fields[0] ?? "";
|
|
938
|
+
const parents = (fields[1] ?? "").split(" ").filter(Boolean);
|
|
939
|
+
const comparisonBase = parents[0] ?? null;
|
|
940
|
+
const common = "--no-ext-diff --no-textconv --find-renames";
|
|
941
|
+
const range = comparisonBase ? quoteShellArg(comparisonBase) + " " + quoteShellArg(resolvedHash) : quoteShellArg(resolvedHash);
|
|
942
|
+
const [nameStatusResult, numstatResult] = await Promise.all([
|
|
943
|
+
this.run(workdir, comparisonBase ? "git diff " + common + " --name-status " + range + " --" : "git diff-tree --root --no-commit-id --name-status -r -M " + range + " --", 2e4, COMMIT_DETAIL_MAX_CHARS, signal, sandboxPolicy),
|
|
944
|
+
this.run(workdir, comparisonBase ? "git diff " + common + " --numstat " + range + " --" : "git diff-tree --root --no-commit-id --numstat -r -M " + range + " --", 2e4, COMMIT_DETAIL_MAX_CHARS, signal, sandboxPolicy)
|
|
945
|
+
]);
|
|
946
|
+
if (nameStatusResult.exitCode !== 0 || numstatResult.exitCode !== 0) {
|
|
947
|
+
return errorResult("GIT_FAILED", "\u65E0\u6CD5\u8BFB\u53D6\u63D0\u4EA4\u53D8\u66F4\u6458\u8981", redactAndLimit(outputOf(nameStatusResult) + "\n" + outputOf(numstatResult), 8192));
|
|
948
|
+
}
|
|
949
|
+
const parsed = parseCommitFiles(nameStatusResult.stdout?.text ?? "", numstatResult.stdout?.text ?? "");
|
|
950
|
+
return {
|
|
951
|
+
ok: true,
|
|
952
|
+
data: {
|
|
953
|
+
hash: redactAndLimit(resolvedHash, 128),
|
|
954
|
+
parents: parents.map((parent) => redactAndLimit(parent, 128)),
|
|
955
|
+
authorName: redactAndLimit(fields[2] ?? "", 512),
|
|
956
|
+
authorEmail: redactAndLimit(fields[3] ?? "", 512),
|
|
957
|
+
authoredAt: redactAndLimit(fields[4] ?? "", 128),
|
|
958
|
+
committerName: redactAndLimit(fields[5] ?? "", 512),
|
|
959
|
+
committerEmail: redactAndLimit(fields[6] ?? "", 512),
|
|
960
|
+
committedAt: redactAndLimit(fields[7] ?? "", 128),
|
|
961
|
+
subject: redactAndLimit(fields[8] ?? "", 4096),
|
|
962
|
+
body: redactAndLimit(fields[9] ?? "", 5e4),
|
|
963
|
+
comparisonBase,
|
|
964
|
+
...parsed
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
async getCommitDiff(workdir, hash, signal, sandboxPolicy) {
|
|
969
|
+
if (!validCommitHash(hash)) return errorResult("INVALID_ARGUMENT", "\u63D0\u4EA4\u54C8\u5E0C\u5FC5\u987B\u662F\u5B8C\u6574\u7684 40 \u6216 64 \u4F4D\u5341\u516D\u8FDB\u5236\u5B57\u7B26");
|
|
970
|
+
const topLevel = await this.getTopLevel(workdir, signal, sandboxPolicy);
|
|
971
|
+
if (!topLevel.ok) return topLevel;
|
|
972
|
+
const parentsResult = await this.run(workdir, "git show -s --format=%P " + quoteShellArg(hash), 15e3, 4096, signal, sandboxPolicy);
|
|
973
|
+
if (parentsResult.exitCode !== 0) return errorResult("GIT_FAILED", "\u65E0\u6CD5\u8BFB\u53D6\u63D0\u4EA4\u7236\u8282\u70B9", redactAndLimit(outputOf(parentsResult), 8192));
|
|
974
|
+
const comparisonBase = (parentsResult.stdout?.text ?? "").trim().split(" ").filter(Boolean)[0] ?? null;
|
|
975
|
+
const common = "--no-ext-diff --no-textconv --find-renames --patch";
|
|
976
|
+
const command = comparisonBase ? "git diff " + common + " " + quoteShellArg(comparisonBase) + " " + quoteShellArg(hash) + " --" : "git show --format= " + common + " " + quoteShellArg(hash) + " --";
|
|
977
|
+
const result = await this.run(workdir, command, 3e4, COMMIT_DIFF_MAX_CHARS + 1024, signal, sandboxPolicy);
|
|
978
|
+
if (result.exitCode !== 0) return errorResult("GIT_FAILED", "\u65E0\u6CD5\u8BFB\u53D6\u63D0\u4EA4 Diff", redactAndLimit(outputOf(result), 8192));
|
|
979
|
+
const raw = redactSecrets(result.stdout?.text ?? "");
|
|
980
|
+
return {
|
|
981
|
+
ok: true,
|
|
982
|
+
data: { hash, comparisonBase, diff: raw.slice(0, COMMIT_DIFF_MAX_CHARS), truncated: raw.length > COMMIT_DIFF_MAX_CHARS }
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
async getStashes(workdir, signal, sandboxPolicy) {
|
|
986
|
+
const topLevel = await this.getTopLevel(workdir, signal, sandboxPolicy);
|
|
987
|
+
if (!topLevel.ok) return topLevel;
|
|
988
|
+
const format = "%gd%x1f%H%x1f%gs%x1f%an%x1f%aI";
|
|
989
|
+
const result = await this.run(
|
|
990
|
+
workdir,
|
|
991
|
+
"git stash list --max-count=" + STASH_MAX + " --format=" + quoteShellArg(format),
|
|
992
|
+
15e3,
|
|
993
|
+
COMMIT_DETAIL_MAX_CHARS,
|
|
994
|
+
signal,
|
|
995
|
+
sandboxPolicy
|
|
996
|
+
);
|
|
997
|
+
if (result.exitCode !== 0) return errorResult("GIT_FAILED", "\u65E0\u6CD5\u8BFB\u53D6\u8D2E\u85CF\u5217\u8868", redactAndLimit(outputOf(result), 8192));
|
|
998
|
+
const stashes = (result.stdout?.text ?? "").split("\n").filter(Boolean).slice(0, STASH_MAX).map((line) => {
|
|
999
|
+
const [selector = "", hash = "", subject = "", author = "", date = ""] = line.split("");
|
|
1000
|
+
return {
|
|
1001
|
+
selector: redactAndLimit(selector, 128),
|
|
1002
|
+
hash: redactAndLimit(hash, 128),
|
|
1003
|
+
subject: redactAndLimit(subject, 4096),
|
|
1004
|
+
author: redactAndLimit(author, 512),
|
|
1005
|
+
date: redactAndLimit(date, 128)
|
|
1006
|
+
};
|
|
1007
|
+
});
|
|
1008
|
+
return { ok: true, data: stashes };
|
|
1009
|
+
}
|
|
1010
|
+
async stagePaths(request, paths) {
|
|
1011
|
+
const valid = this.validatePaths(paths);
|
|
1012
|
+
if (!valid.ok) return valid;
|
|
1013
|
+
return this.mutate(request, "git add -- " + valid.data.map(quoteShellArg).join(" "), false, "\u6682\u5B58\u6587\u4EF6\u5931\u8D25");
|
|
1014
|
+
}
|
|
1015
|
+
async unstagePaths(request, paths) {
|
|
1016
|
+
const valid = this.validatePaths(paths);
|
|
1017
|
+
if (!valid.ok) return valid;
|
|
1018
|
+
return this.mutate(request, "git reset HEAD -- " + valid.data.map(quoteShellArg).join(" "), false, "\u53D6\u6D88\u6682\u5B58\u6587\u4EF6\u5931\u8D25");
|
|
1019
|
+
}
|
|
1020
|
+
async stageAll(request) {
|
|
1021
|
+
return this.mutate(request, "git add -A", false, "\u5168\u90E8\u6682\u5B58\u5931\u8D25");
|
|
1022
|
+
}
|
|
1023
|
+
async unstageAll(request) {
|
|
1024
|
+
return this.mutate(request, "git reset HEAD -- :/", false, "\u53D6\u6D88\u5168\u90E8\u6682\u5B58\u5931\u8D25");
|
|
1025
|
+
}
|
|
1026
|
+
async commit(request, message) {
|
|
1027
|
+
if (typeof message !== "string" || !message.trim() || message.length > 4096 || /[\0\r\n]/.test(message)) {
|
|
1028
|
+
return errorResult("INVALID_ARGUMENT", "\u63D0\u4EA4\u4FE1\u606F\u5FC5\u987B\u4E3A 1\u20134096 \u4E2A\u975E\u6362\u884C\u5B57\u7B26");
|
|
1029
|
+
}
|
|
1030
|
+
return this.mutate(request, "git commit -m " + quoteShellArg(message.trim()), true, "\u63D0\u4EA4\u5931\u8D25");
|
|
1031
|
+
}
|
|
1032
|
+
async createBranch(request, name, base) {
|
|
1033
|
+
if (!validBranchName(name) || !validBranchName(base)) return errorResult("INVALID_ARGUMENT", "\u5206\u652F\u540D\u548C\u57FA\u7840\u5206\u652F\u5FC5\u987B\u662F\u5B89\u5168\u7684\u672C\u5730 Git \u5F15\u7528");
|
|
1034
|
+
return this.mutate(request, "git switch -c " + quoteShellArg(name) + " " + quoteShellArg(base), false, "\u521B\u5EFA\u5206\u652F\u5931\u8D25");
|
|
1035
|
+
}
|
|
1036
|
+
async switchBranch(request, name) {
|
|
1037
|
+
if (!validBranchName(name)) return errorResult("INVALID_ARGUMENT", "\u5206\u652F\u540D\u5FC5\u987B\u662F\u5B89\u5168\u7684\u672C\u5730 Git \u5F15\u7528");
|
|
1038
|
+
return this.mutate(request, "git switch " + quoteShellArg(name), false, "\u5207\u6362\u5206\u652F\u5931\u8D25");
|
|
1039
|
+
}
|
|
1040
|
+
async deleteBranch(request, name, force, confirmRisk) {
|
|
1041
|
+
if (!validBranchName(name)) return errorResult("INVALID_ARGUMENT", "\u5206\u652F\u540D\u5FC5\u987B\u662F\u5B89\u5168\u7684\u672C\u5730 Git \u5F15\u7528");
|
|
1042
|
+
const topLevel = await this.getTopLevel(request.workdir, request.signal, request.sandboxPolicy);
|
|
1043
|
+
if (!topLevel.ok) return topLevel;
|
|
1044
|
+
const [currentResult, existsResult] = await Promise.all([
|
|
1045
|
+
this.run(request.workdir, "git branch --show-current", 15e3, 4096, request.signal, request.sandboxPolicy),
|
|
1046
|
+
this.run(request.workdir, "git show-ref --verify --quiet " + quoteShellArg("refs/heads/" + name), 15e3, 4096, request.signal, request.sandboxPolicy)
|
|
1047
|
+
]);
|
|
1048
|
+
if (currentResult.exitCode !== 0) return errorResult("GIT_FAILED", "\u65E0\u6CD5\u68C0\u67E5\u5F53\u524D\u5206\u652F", redactAndLimit(outputOf(currentResult), 8192));
|
|
1049
|
+
if (redactAndLimit(currentResult.stdout?.text ?? "", 4096).trim() === name) {
|
|
1050
|
+
return errorResult("STATE_CONFLICT", "\u5F53\u524D\u5206\u652F\u4E0D\u80FD\u5220\u9664\uFF0C\u8BF7\u5148\u5207\u6362\u5230\u5176\u4ED6\u5206\u652F", void 0, "CURRENT_BRANCH");
|
|
1051
|
+
}
|
|
1052
|
+
if (existsResult.exitCode !== 0) return errorResult("STATE_CONFLICT", "\u8981\u5220\u9664\u7684\u672C\u5730\u5206\u652F\u4E0D\u5B58\u5728", void 0, "BRANCH_NOT_FOUND");
|
|
1053
|
+
if (force && !confirmRisk) return errorResult("PERMISSION_DENIED", "\u5F3A\u5236\u5220\u9664\u524D\u5FC5\u987B\u786E\u8BA4\u672A\u5408\u5E76\u63D0\u4EA4\u53EF\u80FD\u6C38\u4E45\u4E22\u5931");
|
|
1054
|
+
const command = "git branch " + (force ? "-D" : "-d") + " -- " + quoteShellArg(name);
|
|
1055
|
+
const deleted = await this.mutate(request, command, false, force ? "\u5F3A\u5236\u5220\u9664\u5206\u652F\u5931\u8D25" : "\u5B89\u5168\u5220\u9664\u5206\u652F\u5931\u8D25");
|
|
1056
|
+
if (deleted.ok || force || deleted.code !== "GIT_FAILED") return deleted;
|
|
1057
|
+
const merged = await this.run(request.workdir, "git merge-base --is-ancestor " + quoteShellArg(name) + " HEAD", 15e3, 4096, request.signal, request.sandboxPolicy);
|
|
1058
|
+
if (merged.exitCode === 1) {
|
|
1059
|
+
return errorResult("STATE_CONFLICT", "\u5206\u652F\u5305\u542B\u5C1A\u672A\u5408\u5E76\u7684\u63D0\u4EA4\uFF0C\u5B89\u5168\u5220\u9664\u5DF2\u62D2\u7EDD", deleted.diagnostics, "UNMERGED_BRANCH");
|
|
1060
|
+
}
|
|
1061
|
+
return deleted;
|
|
1062
|
+
}
|
|
1063
|
+
validatePaths(paths) {
|
|
1064
|
+
if (!Array.isArray(paths) || paths.length === 0 || paths.length > MAX_PATHS || !paths.every(validPathspec)) {
|
|
1065
|
+
return errorResult("INVALID_ARGUMENT", "paths \u5FC5\u987B\u5305\u542B 1\u2013100 \u4E2A\u4ED3\u5E93\u5185\u76F8\u5BF9\u8DEF\u5F84");
|
|
1066
|
+
}
|
|
1067
|
+
return { ok: true, data: paths };
|
|
1068
|
+
}
|
|
1069
|
+
async mutate(request, command, requiresStagedContent = false, failureMessage = "Git \u64CD\u4F5C\u5931\u8D25") {
|
|
1070
|
+
if (!request.sessionId || !request.workdir) return errorResult("SESSION_NOT_FOUND", "\u65E0\u6CD5\u786E\u5B9A\u5F53\u524D\u4F1A\u8BDD\u7684\u4ED3\u5E93\u76EE\u5F55");
|
|
1071
|
+
if (typeof request.operationId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(request.operationId)) {
|
|
1072
|
+
return errorResult("INVALID_ARGUMENT", "operationId \u5FC5\u987B\u662F 1\u2013128 \u4E2A\u5B89\u5168\u5B57\u7B26");
|
|
1073
|
+
}
|
|
1074
|
+
this.pruneOperations();
|
|
1075
|
+
const repository = await this.getTopLevel(request.workdir, request.signal, request.sandboxPolicy);
|
|
1076
|
+
if (!repository.ok) return repository;
|
|
1077
|
+
const key = request.sessionId + "\0" + repository.data.topLevel + "\0" + request.operationId;
|
|
1078
|
+
const existing = this.operations.get(key);
|
|
1079
|
+
if (existing) return existing.result;
|
|
1080
|
+
const lockKey = request.sessionId + "\0" + repository.data.topLevel;
|
|
1081
|
+
const previous = this.locks.get(lockKey) ?? Promise.resolve();
|
|
1082
|
+
let release = () => {
|
|
1083
|
+
};
|
|
1084
|
+
const current = new Promise((resolve) => {
|
|
1085
|
+
release = resolve;
|
|
1086
|
+
});
|
|
1087
|
+
const queued = previous.then(() => current);
|
|
1088
|
+
this.locks.set(lockKey, queued);
|
|
1089
|
+
const result = previous.then(async () => {
|
|
1090
|
+
try {
|
|
1091
|
+
if (requiresStagedContent) {
|
|
1092
|
+
const staged = await this.run(request.workdir, "git diff --cached --quiet", 15e3, 4096, request.signal, request.sandboxPolicy);
|
|
1093
|
+
if (staged.exitCode === 0) return errorResult("STATE_CONFLICT", "\u6CA1\u6709\u5DF2\u6682\u5B58\u7684\u6539\u52A8\uFF0C\u65E0\u6CD5\u63D0\u4EA4");
|
|
1094
|
+
if (staged.exitCode !== 1) return errorResult("GIT_FAILED", "\u65E0\u6CD5\u68C0\u67E5\u6682\u5B58\u533A", redactAndLimit(outputOf(staged), 8192));
|
|
1095
|
+
}
|
|
1096
|
+
const executed = await this.run(request.workdir, command, 12e4, MUTATION_OUTPUT_MAX_CHARS, request.signal, request.sandboxPolicy);
|
|
1097
|
+
if (executed.exitCode !== 0) return errorResult(mutationErrorCode(executed), failureMessage, redactAndLimit(outputOf(executed), 8192));
|
|
1098
|
+
const summary = await this.getSummary(request.workdir, request.signal, request.sandboxPolicy);
|
|
1099
|
+
return summary.ok ? { ...summary, operationId: String(request.operationId) } : summary;
|
|
1100
|
+
} finally {
|
|
1101
|
+
release();
|
|
1102
|
+
if (this.locks.get(lockKey) === queued) this.locks.delete(lockKey);
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
this.operations.set(key, { createdAt: Date.now(), result });
|
|
1106
|
+
return result;
|
|
1107
|
+
}
|
|
1108
|
+
pruneOperations(now = Date.now()) {
|
|
1109
|
+
for (const [key, entry] of this.operations) if (now - entry.createdAt > OPERATION_TTL_MS) this.operations.delete(key);
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
// src/host/plugin.ts
|
|
1116
|
+
var require_plugin = __commonJS({
|
|
1117
|
+
"src/host/plugin.ts"(exports2, module2) {
|
|
1118
|
+
"use strict";
|
|
1119
|
+
init_actions();
|
|
1120
|
+
init_proposal_service();
|
|
1121
|
+
init_git_repository_service();
|
|
1122
|
+
init_command_policy();
|
|
1123
|
+
var MAX_STEPS = 10;
|
|
1124
|
+
function sessionIdOf(exec) {
|
|
1125
|
+
try {
|
|
1126
|
+
const agent = exec && exec.agent;
|
|
1127
|
+
if (agent && typeof agent.id === "string" && agent.id.trim() && agent.id.trim().length <= 200) return agent.id.trim();
|
|
1128
|
+
} catch (e) {
|
|
1129
|
+
}
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
function sessionWorkdir(exec, args, ctx) {
|
|
1133
|
+
try {
|
|
1134
|
+
if (args && typeof args.workdir === "string" && args.workdir.trim()) return args.workdir.trim();
|
|
1135
|
+
} catch (e) {
|
|
1136
|
+
}
|
|
1137
|
+
try {
|
|
1138
|
+
const agent = exec && exec.agent;
|
|
1139
|
+
const session = agent && agent.session;
|
|
1140
|
+
const header = session && session.header;
|
|
1141
|
+
const cwd = session && session.cwd || header && header.cwd;
|
|
1142
|
+
if (typeof cwd === "string" && cwd) return cwd;
|
|
1143
|
+
} catch (e) {
|
|
1144
|
+
}
|
|
1145
|
+
try {
|
|
1146
|
+
const sp = ctx.get("sandboxPolicy");
|
|
1147
|
+
if (sp && typeof sp.workspaceRoot === "string" && sp.workspaceRoot) return sp.workspaceRoot;
|
|
1148
|
+
} catch (e) {
|
|
1149
|
+
}
|
|
1150
|
+
return void 0;
|
|
1151
|
+
}
|
|
1152
|
+
function repositoryContextForSession(ctx, sandboxPolicy, sessionId) {
|
|
1153
|
+
try {
|
|
1154
|
+
const agents = ctx.get("agents");
|
|
1155
|
+
const agent = agents && agents.get(sessionId);
|
|
1156
|
+
if (!agent) return null;
|
|
1157
|
+
const workdir = sessionWorkdir({ agent }, {}, ctx);
|
|
1158
|
+
if (!workdir) return null;
|
|
1159
|
+
const policy = sandboxPolicy ? sandboxPolicy.resolve({ session: agent.session }) : void 0;
|
|
1160
|
+
return { workdir, policy };
|
|
1161
|
+
} catch (e) {
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
async function runGit(shell, workdir, command, timeoutMs, stdoutMaxBytes, signal, policy) {
|
|
1166
|
+
try {
|
|
1167
|
+
const spec = shell.resolve({ command, workdir, timeoutMs, stdoutMaxBytes, signal, ...policy ? { sandboxPolicy: policy } : {} });
|
|
1168
|
+
return await shell.run(spec);
|
|
1169
|
+
} catch (err) {
|
|
1170
|
+
return { exitCode: -1, signal: null, timedOut: false, aborted: false, stdout: { text: "" }, stderr: { text: errorMessage2(err) } };
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function errorMessage2(error) {
|
|
1174
|
+
return error instanceof Error ? error.message : String(error);
|
|
1175
|
+
}
|
|
1176
|
+
async function captureFingerprint(shell, workdir) {
|
|
1177
|
+
if (!shell) return null;
|
|
1178
|
+
const command = "echo '--B--'; git branch --show-current 2>&1; echo '--H--'; git rev-parse HEAD 2>&1; echo '--S--'; git status --short 2>&1; echo '--L--'; git log --oneline -3 2>&1; echo '--T--'; git stash list 2>&1";
|
|
1179
|
+
const r = await runGit(shell, workdir, command, 15e3, 2e4);
|
|
1180
|
+
return ((r.stdout?.text ?? "") + (r.stderr?.text ?? "")).trim();
|
|
1181
|
+
}
|
|
1182
|
+
async function captureDiagnostics(shell, workdir) {
|
|
1183
|
+
if (!shell) return "";
|
|
1184
|
+
const command = "echo '--STATUS--'; git status --short --branch 2>&1; echo '--LOG--'; git log --oneline -3 2>&1; echo '--BRANCH--'; git branch -vv 2>&1; echo '--REMOTE--'; git remote -v 2>&1";
|
|
1185
|
+
const r = await runGit(shell, workdir, command, 15e3, 2e4);
|
|
1186
|
+
return redactAndLimit(((r.stdout?.text ?? "") + (r.stderr?.text ?? "")).trim());
|
|
1187
|
+
}
|
|
1188
|
+
async function runChecks(shell, workdir, checks) {
|
|
1189
|
+
if (!shell) return checks.map((check) => check.label);
|
|
1190
|
+
const failed = [];
|
|
1191
|
+
for (const c of checks) {
|
|
1192
|
+
let pass = false;
|
|
1193
|
+
try {
|
|
1194
|
+
if (c.type === "branch") {
|
|
1195
|
+
const r = await runGit(shell, workdir, "git branch --show-current", 1e4, 4096);
|
|
1196
|
+
pass = (r.stdout?.text ?? "").trim() === c.value;
|
|
1197
|
+
} else if (c.type === "commit-msg") {
|
|
1198
|
+
const r = await runGit(shell, workdir, "git log -1 --pretty=%s", 1e4, 4096);
|
|
1199
|
+
pass = (r.stdout?.text ?? "").trim() === c.value;
|
|
1200
|
+
} else if (c.type === "staged") {
|
|
1201
|
+
const paths = Array.isArray(c.value) ? c.value : String(c.value).split(/\s+/).filter(Boolean);
|
|
1202
|
+
pass = true;
|
|
1203
|
+
for (const path of paths) {
|
|
1204
|
+
const r = await runGit(shell, workdir, "git diff --cached --quiet -- " + quoteShellArg(path), 1e4, 4096);
|
|
1205
|
+
if (r.exitCode !== 1) {
|
|
1206
|
+
pass = false;
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
} else if (c.type === "branch-gone") {
|
|
1211
|
+
const r = await runGit(shell, workdir, "git branch --list " + quoteShellArg(c.value), 1e4, 4096);
|
|
1212
|
+
pass = (r.stdout?.text ?? "").trim() === "";
|
|
1213
|
+
} else if (c.type === "stash-nonempty") {
|
|
1214
|
+
const r = await runGit(shell, workdir, "git stash list", 1e4, 4096);
|
|
1215
|
+
pass = (r.stdout?.text ?? "").trim().length > 0;
|
|
1216
|
+
} else if (c.type === "stash-empty") {
|
|
1217
|
+
const r = await runGit(shell, workdir, "git stash list", 1e4, 4096);
|
|
1218
|
+
pass = (r.stdout?.text ?? "").trim().length === 0;
|
|
1219
|
+
} else if (c.type === "clean") {
|
|
1220
|
+
const paths = Array.isArray(c.value) ? c.value : String(c.value).split(/\s+/).filter(Boolean);
|
|
1221
|
+
const r = await runGit(shell, workdir, "git status --porcelain -- " + paths.map(quoteShellArg).join(" "), 1e4, 4096);
|
|
1222
|
+
pass = r.exitCode === 0 && (r.stdout?.text ?? "").trim() === "";
|
|
1223
|
+
} else if (c.type === "no-ahead") {
|
|
1224
|
+
const r = await runGit(shell, workdir, "git rev-list --count @{u}..HEAD 2>&1", 1e4, 4096);
|
|
1225
|
+
pass = (r.stdout?.text ?? "").trim() === "0";
|
|
1226
|
+
}
|
|
1227
|
+
} catch (e) {
|
|
1228
|
+
pass = false;
|
|
1229
|
+
}
|
|
1230
|
+
if (!pass) failed.push(c.label);
|
|
1231
|
+
}
|
|
1232
|
+
return failed;
|
|
1233
|
+
}
|
|
1234
|
+
async function verifyProposal(shell, proposal) {
|
|
1235
|
+
const now = await captureFingerprint(shell, proposal.workdir);
|
|
1236
|
+
const changed = now !== null && proposal.fingerprint !== null && now !== proposal.fingerprint;
|
|
1237
|
+
const visibleState = redactAndLimit(now || "");
|
|
1238
|
+
const checks = deriveChecks(proposal.steps.map((s) => s.command));
|
|
1239
|
+
if (checks.length === 0) {
|
|
1240
|
+
return {
|
|
1241
|
+
changed,
|
|
1242
|
+
verified: false,
|
|
1243
|
+
partial: changed,
|
|
1244
|
+
message: changed ? "\u68C0\u6D4B\u5230\u4ED3\u5E93\u72B6\u6001\u53D8\u5316\uFF0C\u4F46\u8BE5\u547D\u4EE4\u6CA1\u6709\u53EF\u53EF\u9760\u6BD4\u5BF9\u7684\u76EE\u6807\u72B6\u6001\uFF0C\u65E0\u6CD5\u786E\u8BA4\u53D8\u5316\u6765\u81EA\u672C\u5EFA\u8BAE" : "\u672A\u68C0\u6D4B\u5230\u4ED3\u5E93\u72B6\u6001\u53D8\u5316\uFF0C\u770B\u8D77\u6765\u8FD8\u6CA1\u6709\u6267\u884C",
|
|
1245
|
+
changedState: changed ? visibleState : ""
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
const failed = await runChecks(shell, proposal.workdir, checks);
|
|
1249
|
+
const baselineFailed = Array.isArray(proposal.baselineFailed) ? proposal.baselineFailed : [];
|
|
1250
|
+
const transitioned = baselineFailed.length > 0 && failed.length === 0;
|
|
1251
|
+
if (transitioned) return { changed, verified: true, partial: false, message: "", changedState: visibleState };
|
|
1252
|
+
if (failed.length === 0) {
|
|
1253
|
+
return {
|
|
1254
|
+
changed,
|
|
1255
|
+
verified: false,
|
|
1256
|
+
partial: changed,
|
|
1257
|
+
message: "\u590D\u5236\u547D\u4EE4\u65F6\u76EE\u6807\u72B6\u6001\u5DF2\u7ECF\u6EE1\u8DB3\uFF0C\u65E0\u6CD5\u636E\u6B64\u786E\u8BA4\u672C\u6B21\u662F\u5426\u6267\u884C\uFF1B\u8BF7\u5728\u7EC8\u7AEF\u6838\u5BF9\u7ED3\u679C",
|
|
1258
|
+
changedState: changed ? visibleState : ""
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
const progress = changed || failed.length < baselineFailed.length;
|
|
1262
|
+
return {
|
|
1263
|
+
changed,
|
|
1264
|
+
verified: false,
|
|
1265
|
+
partial: progress,
|
|
1266
|
+
message: (progress ? "\u68C0\u6D4B\u5230\u72B6\u6001\u53D8\u5316\uFF0C\u4F46\u9884\u671F\u7ED3\u679C\u5C1A\u672A\u5168\u90E8\u8FBE\u6210\uFF1A" : "\u672A\u68C0\u6D4B\u5230\u9884\u671F\u7ED3\u679C\uFF1A") + failed.join("\uFF1B"),
|
|
1267
|
+
changedState: changed ? visibleState : ""
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
async function executeProposalSteps(shell, proposal, signal, policy) {
|
|
1271
|
+
let ok = true;
|
|
1272
|
+
const stepsResult = [];
|
|
1273
|
+
for (const step of proposal.steps) {
|
|
1274
|
+
const validated = validateCommand(step.command);
|
|
1275
|
+
if (!validated.ok) {
|
|
1276
|
+
step.result = { ok: false, exitCode: -1, signal: "", timedOut: false, stdout: "", stderr: validated.error };
|
|
1277
|
+
stepsResult.push({ command: step.command, ok: false, exitCode: -1 });
|
|
1278
|
+
ok = false;
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
const r = await runGit(shell, proposal.workdir, validated.normalized, 12e4, 1e5, signal, policy);
|
|
1282
|
+
const sok = r.exitCode === 0;
|
|
1283
|
+
const result = {
|
|
1284
|
+
ok: sok,
|
|
1285
|
+
exitCode: r.exitCode === null ? -1 : r.exitCode,
|
|
1286
|
+
signal: r.signal || "",
|
|
1287
|
+
timedOut: r.timedOut === true,
|
|
1288
|
+
stdout: redactAndLimit(r.stdout?.text ?? ""),
|
|
1289
|
+
stderr: redactAndLimit(r.stderr?.text ?? "")
|
|
1290
|
+
};
|
|
1291
|
+
step.result = result;
|
|
1292
|
+
stepsResult.push({ command: step.command, ok: sok, exitCode: result.exitCode });
|
|
1293
|
+
if (!sok) {
|
|
1294
|
+
ok = false;
|
|
1295
|
+
break;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
proposal.result = { ok };
|
|
1299
|
+
return { ok, stepsResult };
|
|
1300
|
+
}
|
|
1301
|
+
function executionError(proposal, error) {
|
|
1302
|
+
return {
|
|
1303
|
+
ok: false,
|
|
1304
|
+
proposalId: proposal ? proposal.proposalId : "",
|
|
1305
|
+
command: proposal ? proposal.command : "",
|
|
1306
|
+
steps: [],
|
|
1307
|
+
exitCode: -1,
|
|
1308
|
+
signal: "",
|
|
1309
|
+
timedOut: false,
|
|
1310
|
+
stdout: "",
|
|
1311
|
+
stderr: "",
|
|
1312
|
+
diagnostics: "",
|
|
1313
|
+
error
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
async function executeRegisteredProposal(shell, proposal, signal, policy, persistStatus) {
|
|
1317
|
+
if (!proposal) return executionError(null, "\u627E\u4E0D\u5230\u8BE5\u63D0\u8BAE\uFF08proposalId \u65E0\u6548\u6216\u5DF2\u8FC7\u671F\uFF09");
|
|
1318
|
+
if (proposal.closed === true || proposal.status === "succeeded" || proposal.status === "failed" || proposal.status === "verified" || proposal.status === "dismissed") {
|
|
1319
|
+
return executionError(proposal, "\u8BE5\u63D0\u8BAE\u5DF2\u7ECF\u7ED3\u675F\uFF0C\u4E0D\u80FD\u91CD\u590D\u6267\u884C\uFF1B\u5982\u9700\u91CD\u8BD5\uFF0C\u8BF7\u521B\u5EFA\u65B0\u7684\u63D0\u8BAE");
|
|
1320
|
+
}
|
|
1321
|
+
if (proposal.status === "running") return executionError(proposal, "\u8BE5\u63D0\u8BAE\u6B63\u5728\u6267\u884C\uFF0C\u8BF7\u52FF\u91CD\u590D\u63D0\u4EA4");
|
|
1322
|
+
if (!shell) return executionError(proposal, "shell \u670D\u52A1\u4E0D\u53EF\u7528");
|
|
1323
|
+
proposal.status = "running";
|
|
1324
|
+
proposal.startedAt = Date.now();
|
|
1325
|
+
if (typeof persistStatus === "function") await persistStatus();
|
|
1326
|
+
const { ok, stepsResult } = await executeProposalSteps(shell, proposal, signal, policy);
|
|
1327
|
+
proposal.status = ok ? "succeeded" : "failed";
|
|
1328
|
+
proposal.finishedAt = Date.now();
|
|
1329
|
+
const last = proposal.steps[proposal.steps.length - 1];
|
|
1330
|
+
const failedStep = proposal.steps.find((step) => step.result?.ok === false);
|
|
1331
|
+
const lastResult = failedStep ? failedStep.result : last?.result;
|
|
1332
|
+
const diagnostics = ok ? "" : await captureDiagnostics(shell, proposal.workdir);
|
|
1333
|
+
const recovery = ok ? null : buildRecovery(proposal, failedStep, diagnostics);
|
|
1334
|
+
if (recovery && recovery.command) {
|
|
1335
|
+
const corrected = registerRecoveryProposal(proposal, recovery);
|
|
1336
|
+
if (corrected) recovery.proposalId = corrected.proposalId;
|
|
1337
|
+
}
|
|
1338
|
+
if (typeof persistStatus === "function") await persistStatus();
|
|
1339
|
+
return {
|
|
1340
|
+
ok,
|
|
1341
|
+
proposalId: proposal.proposalId,
|
|
1342
|
+
command: proposal.command,
|
|
1343
|
+
steps: stepsResult,
|
|
1344
|
+
exitCode: lastResult ? lastResult.exitCode : -1,
|
|
1345
|
+
signal: lastResult ? lastResult.signal : "",
|
|
1346
|
+
timedOut: lastResult ? lastResult.timedOut : false,
|
|
1347
|
+
stdout: redactSecrets(lastResult ? lastResult.stdout : ""),
|
|
1348
|
+
stderr: redactSecrets(lastResult ? lastResult.stderr : ""),
|
|
1349
|
+
diagnostics,
|
|
1350
|
+
recovery: recovery ? { suggestion: recovery.suggestion, command: recovery.command || "", proposalId: recovery.proposalId || null } : null,
|
|
1351
|
+
error: ok ? "" : redactSecrets(lastResult && (lastResult.stderr || lastResult.stdout) || "git \u9000\u51FA\u7801 " + (lastResult ? lastResult.exitCode : -1))
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
function buildRecovery(_proposal, failedStep, diagnostics) {
|
|
1355
|
+
if (!failedStep || !failedStep.result) return null;
|
|
1356
|
+
const text = String(((failedStep.result.stderr || "") + " " + (failedStep.result.stdout || "") + " " + (diagnostics || "")).trim());
|
|
1357
|
+
const cmd = String(failedStep.command || "");
|
|
1358
|
+
if (/只读文件系统|read-only file system|EROFS|cannot lock ref|cannot create .*\.lock/i.test(text)) {
|
|
1359
|
+
return { suggestion: "\u6267\u884C\u73AF\u5883\u5BF9\u76EE\u6807\u76EE\u5F55\u53EA\u8BFB\uFF08\u6C99\u7BB1\u7B56\u7565\u6216\u6302\u8F7D\u95EE\u9898\uFF09\uFF1A\u8BF7\u5728\u7EC8\u7AEF\u624B\u52A8\u6267\u884C\u8BE5\u547D\u4EE4\uFF0C\u6216\u8C03\u6574\u6267\u884C\u73AF\u5883\u7684\u6C99\u7BB1\u6743\u9650\u3002", command: null };
|
|
1360
|
+
}
|
|
1361
|
+
if (/(\.gitignore|被忽略|ignored by your|did not match any files|没有匹配任何文件)/i.test(text) && /git\s+add\b/.test(cmd)) {
|
|
1362
|
+
const corrected = cmd.replace(/git\s+add\s+/, "git add -f ");
|
|
1363
|
+
if (corrected !== cmd) return { suggestion: "\u76EE\u6807\u6587\u4EF6\u88AB .gitignore \u5FFD\u7565\uFF1A\u6539\u7528 -f \u5F3A\u5236\u52A0\u5165\uFF08\u4EC5\u9488\u5BF9\u660E\u786E\u5217\u51FA\u7684\u6587\u4EF6\uFF09\u3002", command: corrected };
|
|
1364
|
+
}
|
|
1365
|
+
if (/already exists|分支.*已存在|already exist/i.test(text)) {
|
|
1366
|
+
const m = cmd.match(/git\s+(?:switch\s+-c|checkout\s+-b)\s+([^\s]+)/);
|
|
1367
|
+
if (m) {
|
|
1368
|
+
const corrected = cmd.replace(/switch\s+-c/, "switch").replace(/checkout\s+-b/, "checkout");
|
|
1369
|
+
return { suggestion: "\u5206\u652F " + m[1] + " \u5DF2\u5B58\u5728\uFF1A\u6539\u4E3A\u5207\u6362\u5230\u73B0\u6709\u5206\u652F\uFF08\u6216\u6362\u4E00\u4E2A\u5206\u652F\u540D\uFF09\u3002", command: corrected };
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (/not a git repository|不是.*git 仓库|不是一个 git 仓库/i.test(text)) {
|
|
1373
|
+
return { suggestion: "\u76EE\u6807\u76EE\u5F55\u4E0D\u662F git \u4ED3\u5E93\uFF1A\u786E\u8BA4 workdir \u6307\u5411\u4ED3\u5E93\u6839\u76EE\u5F55\uFF0C\u6216\u5148 git init\u3002", command: null };
|
|
1374
|
+
}
|
|
1375
|
+
if (/Bad owner or permissions|Could not resolve hostname|Permission denied \(publickey\)|ssh:|Connection (refused|timed out)/i.test(text)) {
|
|
1376
|
+
return { suggestion: "SSH/\u8FDC\u7A0B\u8FDE\u63A5\u5931\u8D25\uFF1A\u68C0\u67E5 ssh \u914D\u7F6E\u4E0E\u5BC6\u94A5\uFF08\u914D\u7F6E\u6587\u4EF6\u6743\u9650\u3001\u5BC6\u94A5\u662F\u5426\u88AB\u6388\u6743\uFF09\uFF0C\u53EF\u4E34\u65F6\u7528 git -c core.sshCommand \u8986\u76D6 ssh \u53C2\u6570\u3002", command: null };
|
|
1377
|
+
}
|
|
1378
|
+
if (/(rejected|failed to push|non-fast-forward|远程.*拒绝|推送.*失败)/i.test(text) && /git\s+push\b/.test(cmd)) {
|
|
1379
|
+
return { suggestion: "\u63A8\u9001\u88AB\u62D2\u7EDD\uFF1A\u5148 git pull --rebase \u540C\u6B65\u8FDC\u7A0B\u518D\u91CD\u8BD5\uFF1B\u82E5\u786E\u8BA4\u8981\u8986\u76D6\u8FDC\u7A0B\u5386\u53F2\uFF0C\u9700\u660E\u786E\u786E\u8BA4\u540E\u4F7F\u7528 --force-with-lease\uFF08\u9AD8\u98CE\u9669\uFF09\u3002", command: null };
|
|
1380
|
+
}
|
|
1381
|
+
if (/(nothing to commit|没有.*要提交|nothing added to commit)/i.test(text) && /git\s+commit\b/.test(cmd)) {
|
|
1382
|
+
return { suggestion: "\u6CA1\u6709\u53EF\u63D0\u4EA4\u7684\u6539\u52A8\uFF1A\u5148 git add \u6682\u5B58\u6587\u4EF6\uFF08\u6CE8\u610F\u88AB .gitignore \u5FFD\u7565\u7684\u6587\u4EF6\u9700 -f\uFF09\u3002", command: null };
|
|
1383
|
+
}
|
|
1384
|
+
if (/CONFLICT|冲突|conflict/i.test(text)) {
|
|
1385
|
+
return { suggestion: "\u5B58\u5728\u5408\u5E76\u51B2\u7A81\uFF1A\u5148\u89E3\u51B3\u51B2\u7A81\u6587\u4EF6\uFF0C\u518D git add \u6807\u8BB0\u4E3A\u5DF2\u89E3\u51B3\uFF0C\u6700\u540E git commit \u5B8C\u6210\u5408\u5E76\u3002", command: null };
|
|
1386
|
+
}
|
|
1387
|
+
if (/(No upstream|no upstream|没有上游|no tracking)/i.test(text)) {
|
|
1388
|
+
return { suggestion: "\u5206\u652F\u6CA1\u6709\u4E0A\u6E38\u8DDF\u8E2A\uFF1A\u7528 git push -u origin <\u5206\u652F\u540D> \u5EFA\u7ACB\u8DDF\u8E2A\u540E\u91CD\u8BD5\u3002", command: null };
|
|
1389
|
+
}
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1392
|
+
function registerRecoveryProposal(failedProposal, recovery) {
|
|
1393
|
+
if (!recovery.command) return null;
|
|
1394
|
+
const v = validateCommand(recovery.command);
|
|
1395
|
+
if (!v.ok) return null;
|
|
1396
|
+
const sessionId = failedProposal.sessionId;
|
|
1397
|
+
const prev = proposalService.list(sessionId);
|
|
1398
|
+
if (prev && prev.some((p) => p.status === "running")) return null;
|
|
1399
|
+
const risk = classifyRisk(recovery.command);
|
|
1400
|
+
const proposal = {
|
|
1401
|
+
proposalId: proposalService.newId(),
|
|
1402
|
+
sessionId,
|
|
1403
|
+
intent: "\u4FEE\u6B63\u5EFA\u8BAE\uFF1A" + recovery.suggestion,
|
|
1404
|
+
command: recovery.command.trim(),
|
|
1405
|
+
steps: [{ command: recovery.command.trim(), result: null }],
|
|
1406
|
+
explanation: recovery.suggestion + "\uFF08\u7531\u6267\u884C\u5931\u8D25\u81EA\u52A8\u751F\u6210\uFF0C\u8BF7\u786E\u8BA4\u540E\u6267\u884C\uFF09",
|
|
1407
|
+
risk: risk.level,
|
|
1408
|
+
reasons: risk.reasons,
|
|
1409
|
+
confirmed: false,
|
|
1410
|
+
workdir: failedProposal.workdir,
|
|
1411
|
+
createdAt: Date.now(),
|
|
1412
|
+
result: null,
|
|
1413
|
+
closed: false,
|
|
1414
|
+
copied: false,
|
|
1415
|
+
fingerprint: null,
|
|
1416
|
+
verified: false,
|
|
1417
|
+
status: "pending",
|
|
1418
|
+
recovery: true
|
|
1419
|
+
};
|
|
1420
|
+
proposalService.closeOpen(sessionId);
|
|
1421
|
+
storeProposal(sessionId, proposal);
|
|
1422
|
+
console.log("git-guide \u4FEE\u6B63\u5EFA\u8BAE\u767B\u8BB0", proposal.proposalId, "session=", sessionId, "risk=", risk.level);
|
|
1423
|
+
return proposal;
|
|
1424
|
+
}
|
|
1425
|
+
function storeProposal(sessionId, proposal) {
|
|
1426
|
+
return proposalService.store(sessionId, proposal);
|
|
1427
|
+
}
|
|
1428
|
+
function findProposal(sessionId, proposalId) {
|
|
1429
|
+
return proposalService.find(sessionId, proposalId);
|
|
1430
|
+
}
|
|
1431
|
+
function proposalView(proposal) {
|
|
1432
|
+
return proposalService.view(proposal);
|
|
1433
|
+
}
|
|
1434
|
+
function latestPending(sessionId) {
|
|
1435
|
+
return proposalService.latestPending(sessionId);
|
|
1436
|
+
}
|
|
1437
|
+
function connectProposalStorage(ctx) {
|
|
1438
|
+
let storage;
|
|
1439
|
+
try {
|
|
1440
|
+
storage = ctx.get("storage");
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
storage = null;
|
|
1443
|
+
}
|
|
1444
|
+
if (!storage || !storage.backend || typeof storage.backend.get !== "function") return Promise.resolve();
|
|
1445
|
+
let unit;
|
|
1446
|
+
const ready = (async () => {
|
|
1447
|
+
const backend = storage.backend.get("json");
|
|
1448
|
+
if (!backend || !backend.kv || typeof backend.kv.open !== "function") throw new Error("JSON storage backend does not support key-value units");
|
|
1449
|
+
unit = await backend.kv.open({
|
|
1450
|
+
name: "git_guide_proposals",
|
|
1451
|
+
version: 1,
|
|
1452
|
+
tables: ["proposals"],
|
|
1453
|
+
hasGlobal: false
|
|
1454
|
+
});
|
|
1455
|
+
await proposalService.attachStorage(unit);
|
|
1456
|
+
})();
|
|
1457
|
+
if (typeof ctx.effect === "function") {
|
|
1458
|
+
ctx.effect(() => () => ready.then(() => unit ? proposalService.closeStorage(unit) : void 0));
|
|
1459
|
+
}
|
|
1460
|
+
return ready;
|
|
1461
|
+
}
|
|
1462
|
+
var plugin2 = {
|
|
1463
|
+
name: "git-guide",
|
|
1464
|
+
inject: ["shell", "tools"],
|
|
1465
|
+
apply(ctx) {
|
|
1466
|
+
const shell = ctx.get("shell");
|
|
1467
|
+
const tools = ctx.get("tools");
|
|
1468
|
+
const sandboxPolicy = ctx.get("sandboxPolicy");
|
|
1469
|
+
const repository = new GitRepositoryService(shell ?? void 0);
|
|
1470
|
+
const proposalStorageReady = connectProposalStorage(ctx);
|
|
1471
|
+
if (tools) {
|
|
1472
|
+
tools.register({
|
|
1473
|
+
name: "git_propose",
|
|
1474
|
+
description: '\u5F53\u7528\u6237\u7528\u81EA\u7136\u8BED\u8A00\u63CF\u8FF0\u4E00\u4E2A\u60F3\u505A\u7684 git \u64CD\u4F5C\uFF08\u4F46\u4E0D\u77E5\u9053/\u4E0D\u786E\u5B9A\u5177\u4F53\u547D\u4EE4\uFF09\u65F6\uFF0C\u8C03\u7528\u672C\u5DE5\u5177\u63D0\u51FA\u547D\u4EE4\u5EFA\u8BAE\u5E76\u767B\u8BB0\u4E3A\u5F85\u5904\u7406\u63D0\u8BAE\u3002\u53EF\u5148\u8C03\u7528 git_repo_state \u4E86\u89E3\u4ED3\u5E93\u73B0\u72B6\uFF0C\u518D\u9009\u62E9\u6700\u7B80\u6D01\u3001\u6700\u5B89\u5168\u3001\u526F\u4F5C\u7528\u6700\u5C0F\u7684\u7EAF git \u547D\u4EE4\u5E76\u7ED9\u51FA\u6E05\u6670\u4E2D\u6587\u89E3\u91CA\u3002\u5206\u652F\u521B\u5EFA\u6216\u5207\u6362\u5FC5\u987B\u4F18\u5148\u4F7F\u7528 git switch\uFF0C\u6587\u4EF6\u8FD8\u539F\u5FC5\u987B\u4F18\u5148\u4F7F\u7528 git restore\uFF1B\u9664\u975E\u6CA1\u6709\u73B0\u4EE3\u7B49\u4EF7\u547D\u4EE4\uFF0C\u5426\u5219\u4E0D\u8981\u4F7F\u7528\u8BED\u4E49\u542B\u6DF7\u7684 git checkout\u3002\u591A\u6761\u547D\u4EE4\u8BF7\u7528 steps \u6570\u7EC4\u5206\u5F00\u4F20\u5165\uFF08\u5982 ["git add -A", "git commit -m \\"msg\\""]\uFF09\uFF0C\u4E0D\u8981\u7528 && \u62FC\u6210\u4E00\u6761\uFF1B\u63D2\u4EF6\u4F1A\u9010\u6B65\u6267\u884C\u3001\u9010\u6B65\u6821\u9A8C\u3001\u5931\u8D25\u5373\u505C\u3002\u767B\u8BB0\u6210\u529F\u540E\u672C\u8F6E Agent \u4F1A\u7ED3\u675F\uFF0CGit \u5DE5\u4F5C\u53F0\u5C06\u5C55\u793A\u63D0\u8BAE\uFF1B\u6A21\u578B\u4E0D\u5F97\u8BE2\u95EE\u662F\u5426\u6267\u884C\uFF0C\u4E5F\u4E0D\u80FD\u6267\u884C\u63D0\u8BAE\u3002\u9AD8\u98CE\u9669\u63D0\u8BAE\u7684\u660E\u786E\u98CE\u9669\u786E\u8BA4\u7531\u5DE5\u4F5C\u53F0\u4E2D\u7684\u7528\u6237\u64CD\u4F5C\u5B8C\u6210\u3002',
|
|
1475
|
+
parameters: {
|
|
1476
|
+
type: "object",
|
|
1477
|
+
properties: {
|
|
1478
|
+
intent: { type: "string", maxLength: 500, description: "\u7528\u6237\u60F3\u8981\u5B8C\u6210\u7684 git \u64CD\u4F5C\u610F\u56FE\uFF08\u81EA\u7136\u8BED\u8A00\uFF0C\u7B80\u77ED\u63CF\u8FF0\uFF09" },
|
|
1479
|
+
command: { type: "string", maxLength: 800, description: "\u5355\u6761 git \u547D\u4EE4\uFF08steps \u4E3A\u7A7A\u65F6\u5FC5\u586B\uFF09\u3002\u521B\u5EFA/\u5207\u6362\u5206\u652F\u4F7F\u7528 switch\uFF0C\u8FD8\u539F\u6587\u4EF6\u4F7F\u7528 restore\uFF1B\u53EA\u5141\u8BB8\u56FA\u5B9A\u767D\u540D\u5355\u5185\u7684\u7EAF git \u5B50\u547D\u4EE4\uFF0C\u4E0D\u5141\u8BB8\u5168\u5C40\u9009\u9879\u3001\u7BA1\u9053\u3001\u91CD\u5B9A\u5411\u6216 shell \u5C55\u5F00" },
|
|
1480
|
+
steps: { type: "array", maxItems: MAX_STEPS, items: { type: "string", maxLength: 800 }, description: "\u591A\u6761 git \u547D\u4EE4\u6309\u6267\u884C\u987A\u5E8F\u5206\u5F00\u4F20\u5165\uFF08\u63A8\u8350\uFF0C\u4EE3\u66FF && \u62FC\u63A5\uFF09\uFF1B\u4F18\u5148\u4F7F\u7528 switch/restore \u7B49\u804C\u8D23\u660E\u786E\u7684\u73B0\u4EE3\u547D\u4EE4\uFF0C\u6BCF\u6761\u5355\u72EC\u6821\u9A8C\u3001\u9010\u6B65\u6267\u884C\u3001\u5931\u8D25\u5373\u505C" },
|
|
1481
|
+
explanation: { type: "string", maxLength: 4e3, description: "\u4E3A\u4EC0\u4E48\u7528\u8FD9\u4E9B\u547D\u4EE4\uFF1A\u5B83\u4EEC\u505A\u4EC0\u4E48\u3001\u4E3A\u4EC0\u4E48\u6700\u7B80\u6D01\u5B89\u5168\u3001\u6709\u4EC0\u4E48\u526F\u4F5C\u7528" },
|
|
1482
|
+
workdir: { type: "string", maxLength: 4096, description: "git \u4ED3\u5E93\u76EE\u5F55\uFF08\u7EDD\u5BF9\u8DEF\u5F84\uFF09\u3002\u7701\u7565\u65F6\u4F7F\u7528\u5F53\u524D\u4F1A\u8BDD\u7684\u5DE5\u4F5C\u76EE\u5F55" }
|
|
1483
|
+
},
|
|
1484
|
+
required: ["intent", "explanation"]
|
|
1485
|
+
},
|
|
1486
|
+
output: {
|
|
1487
|
+
schema: {
|
|
1488
|
+
type: "object",
|
|
1489
|
+
properties: {
|
|
1490
|
+
ok: { type: "boolean" },
|
|
1491
|
+
proposalId: { type: "string" },
|
|
1492
|
+
intent: { type: "string" },
|
|
1493
|
+
command: { type: "string" },
|
|
1494
|
+
steps: { type: "array", items: { type: "object", properties: { command: { type: "string" } }, additionalProperties: false } },
|
|
1495
|
+
explanation: { type: "string" },
|
|
1496
|
+
risk: { type: "string", enum: ["safe", "normal", "hard"] },
|
|
1497
|
+
reasons: { type: "array", items: { type: "string" } },
|
|
1498
|
+
workdir: { type: "string" },
|
|
1499
|
+
error: { type: "string" }
|
|
1500
|
+
},
|
|
1501
|
+
additionalProperties: false
|
|
1502
|
+
},
|
|
1503
|
+
render(_args, value) {
|
|
1504
|
+
return [{ type: "text", text: JSON.stringify(value, null, 2) }];
|
|
1505
|
+
}
|
|
1506
|
+
},
|
|
1507
|
+
async execute(args, exec) {
|
|
1508
|
+
await proposalStorageReady;
|
|
1509
|
+
const sessionId = sessionIdOf(exec);
|
|
1510
|
+
if (!sessionId) {
|
|
1511
|
+
return { ok: false, proposalId: "", intent: String(args && args.intent || ""), command: "", steps: [], explanation: String(args && args.explanation || ""), risk: "normal", reasons: [], workdir: "", error: "\u5F53\u524D\u5DE5\u5177\u8C03\u7528\u7F3A\u5C11\u4F1A\u8BDD\u8EAB\u4EFD\uFF0C\u62D2\u7EDD\u521B\u5EFA\u65E0\u6CD5\u9694\u79BB\u7684\u63D0\u8BAE" };
|
|
1512
|
+
}
|
|
1513
|
+
args = args || {};
|
|
1514
|
+
if (String(args.intent || "").length > 500 || String(args.explanation || "").length > 4e3 || String(args.workdir || "").length > 4096) {
|
|
1515
|
+
return { ok: false, proposalId: "", intent: "", command: "", steps: [], explanation: "", risk: "normal", reasons: [], workdir: "", error: "\u8F93\u5165\u8FC7\u957F\uFF1Aintent \u6700\u591A 500 \u5B57\u7B26\u3001explanation \u6700\u591A 4000 \u5B57\u7B26\u3001workdir \u6700\u591A 4096 \u5B57\u7B26" };
|
|
1516
|
+
}
|
|
1517
|
+
const workdir = sessionWorkdir(exec, args, ctx);
|
|
1518
|
+
const rawCommands = Array.isArray(args.steps) && args.steps.length ? args.steps : args.command ? [args.command] : [];
|
|
1519
|
+
if (rawCommands.length === 0) {
|
|
1520
|
+
return { ok: false, proposalId: "", intent: String(args.intent || ""), command: "", steps: [], explanation: String(args.explanation || ""), risk: "normal", reasons: [], workdir: workdir || "", error: "command \u6216 steps \u81F3\u5C11\u63D0\u4F9B\u4E00\u4E2A" };
|
|
1521
|
+
}
|
|
1522
|
+
if (rawCommands.length > MAX_STEPS) {
|
|
1523
|
+
return { ok: false, proposalId: "", intent: String(args.intent || ""), command: "", steps: [], explanation: String(args.explanation || ""), risk: "normal", reasons: [], workdir: workdir || "", error: "\u6B65\u9AA4\u8FC7\u591A\uFF08\u6700\u591A " + MAX_STEPS + " \u6B65\uFF09" };
|
|
1524
|
+
}
|
|
1525
|
+
const steps = [];
|
|
1526
|
+
for (const raw of rawCommands) {
|
|
1527
|
+
const modern = modernizeCommand(raw);
|
|
1528
|
+
const v = validateCommand(modern.command);
|
|
1529
|
+
if (!v.ok) {
|
|
1530
|
+
return { ok: false, proposalId: "", intent: String(args.intent || ""), command: String(raw), steps: [], explanation: String(args.explanation || ""), risk: "normal", reasons: [], workdir: workdir || "", error: "\u6B65\u9AA4\u300C" + raw + "\u300D\u6821\u9A8C\u5931\u8D25\uFF1A" + v.error };
|
|
1531
|
+
}
|
|
1532
|
+
if (v.subcommand === "checkout") {
|
|
1533
|
+
return { ok: false, proposalId: "", intent: String(args.intent || ""), command: String(raw), steps: [], explanation: String(args.explanation || ""), risk: "normal", reasons: [], workdir: workdir || "", error: "\u6B65\u9AA4\u300C" + raw + "\u300D\u4ECD\u4F7F\u7528\u8BED\u4E49\u542B\u6DF7\u7684 git checkout\uFF1B\u5207\u6362\u5206\u652F\u8BF7\u6539\u7528 git switch\uFF0C\u8FD8\u539F\u6587\u4EF6\u8BF7\u6539\u7528 git restore" };
|
|
1534
|
+
}
|
|
1535
|
+
steps.push({ command: modern.command, result: null });
|
|
1536
|
+
}
|
|
1537
|
+
if (steps.length === 0) {
|
|
1538
|
+
return { ok: false, proposalId: "", intent: String(args.intent || ""), command: "", steps: [], explanation: String(args.explanation || ""), risk: "normal", reasons: [], workdir: workdir || "", error: "\u6CA1\u6709\u53EF\u6267\u884C\u7684\u547D\u4EE4\u6B65\u9AA4" };
|
|
1539
|
+
}
|
|
1540
|
+
if (shell) {
|
|
1541
|
+
const r = await runGit(shell, workdir, "git rev-parse --show-toplevel", 15e3, 4096, exec.signal);
|
|
1542
|
+
if (r.exitCode !== 0) {
|
|
1543
|
+
const errText = ((r.stderr?.text ?? "") + " " + (r.stdout?.text ?? "")).trim();
|
|
1544
|
+
return { ok: false, proposalId: "", intent: String(args.intent || ""), command: steps.map((s) => s.command).join(" && "), steps: steps.map((s) => ({ command: s.command })), explanation: String(args.explanation || ""), risk: "normal", reasons: [], workdir: workdir || "", error: "\u76EE\u6807\u76EE\u5F55\u4E0D\u662F git \u4ED3\u5E93\uFF08workdir=" + (workdir || "\u9ED8\u8BA4\u5DE5\u4F5C\u76EE\u5F55") + "\uFF09\uFF1A" + errText.slice(0, 200) };
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
const commandTexts = steps.map((s) => s.command);
|
|
1548
|
+
const risk = classifyStepsRisk(commandTexts);
|
|
1549
|
+
const prev = proposalService.list(sessionId);
|
|
1550
|
+
if (prev && prev.some((proposal2) => proposal2.status === "running")) {
|
|
1551
|
+
return { ok: false, proposalId: "", intent: String(args.intent || ""), command: "", steps: [], explanation: String(args.explanation || ""), risk: risk.level, reasons: risk.reasons, workdir: workdir || "", error: "\u540C\u4E00\u4F1A\u8BDD\u5DF2\u6709\u63D0\u8BAE\u6B63\u5728\u6267\u884C\uFF0C\u8BF7\u7B49\u5F85\u6267\u884C\u7ED3\u675F\u540E\u518D\u521B\u5EFA\u65B0\u63D0\u8BAE" };
|
|
1552
|
+
}
|
|
1553
|
+
proposalService.closeOpen(sessionId);
|
|
1554
|
+
const proposal = {
|
|
1555
|
+
proposalId: proposalService.newId(),
|
|
1556
|
+
sessionId,
|
|
1557
|
+
intent: String(args.intent || ""),
|
|
1558
|
+
command: commandTexts.join(" && "),
|
|
1559
|
+
steps,
|
|
1560
|
+
explanation: String(args.explanation || ""),
|
|
1561
|
+
risk: risk.level,
|
|
1562
|
+
reasons: risk.reasons,
|
|
1563
|
+
confirmed: false,
|
|
1564
|
+
workdir: workdir || "",
|
|
1565
|
+
createdAt: Date.now(),
|
|
1566
|
+
result: null,
|
|
1567
|
+
closed: false,
|
|
1568
|
+
copied: false,
|
|
1569
|
+
fingerprint: null,
|
|
1570
|
+
verified: false,
|
|
1571
|
+
status: "pending"
|
|
1572
|
+
};
|
|
1573
|
+
storeProposal(sessionId, proposal);
|
|
1574
|
+
await proposalService.flush(sessionId);
|
|
1575
|
+
if (typeof exec.concludeTurn === "function") exec.concludeTurn();
|
|
1576
|
+
console.log("git_propose \u767B\u8BB0", proposal.proposalId, "session=", sessionId, "risk=", risk.level, "steps=", steps.length);
|
|
1577
|
+
return { ok: true, proposalId: proposal.proposalId, intent: proposal.intent, command: proposal.command, steps: steps.map((s) => ({ command: s.command })), explanation: proposal.explanation, risk: risk.level, reasons: risk.reasons, workdir: proposal.workdir, error: "" };
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1580
|
+
tools.register({
|
|
1581
|
+
name: "git_repo_state",
|
|
1582
|
+
description: "\u8BFB\u53D6\u5F53\u524D git \u4ED3\u5E93\u7684\u53EA\u8BFB\u72B6\u6001\uFF08\u9876\u5C42\u76EE\u5F55\u3001\u5F53\u524D\u5206\u652F\u3001\u5DE5\u4F5C\u533A\u72B6\u6001\u3001\u6700\u8FD1\u63D0\u4EA4\u3001stash\u3001\u8FDC\u7A0B\uFF09\uFF0C\u7528\u4E8E\u5728\u63D0\u51FA\u547D\u4EE4\u5EFA\u8BAE\u524D\u4E86\u89E3\u4ED3\u5E93\u73B0\u72B6\u3002\u53EA\u8BFB\uFF0C\u4E0D\u4FEE\u6539\u4EFB\u4F55\u4E1C\u897F\u3002",
|
|
1583
|
+
parameters: {
|
|
1584
|
+
type: "object",
|
|
1585
|
+
properties: {
|
|
1586
|
+
workdir: { type: "string", description: "git \u4ED3\u5E93\u76EE\u5F55\uFF08\u7EDD\u5BF9\u8DEF\u5F84\uFF09\u3002\u7701\u7565\u65F6\u4F7F\u7528\u5F53\u524D\u4F1A\u8BDD\u7684\u5DE5\u4F5C\u76EE\u5F55" }
|
|
1587
|
+
}
|
|
1588
|
+
},
|
|
1589
|
+
output: {
|
|
1590
|
+
schema: {
|
|
1591
|
+
type: "object",
|
|
1592
|
+
properties: {
|
|
1593
|
+
ok: { type: "boolean" },
|
|
1594
|
+
isRepo: { type: "boolean" },
|
|
1595
|
+
workdir: { type: "string" },
|
|
1596
|
+
topLevel: { type: "string" },
|
|
1597
|
+
branch: { type: "string" },
|
|
1598
|
+
status: { type: "string" },
|
|
1599
|
+
recentCommits: { type: "string" },
|
|
1600
|
+
stashes: { type: "string" },
|
|
1601
|
+
remotes: { type: "string" },
|
|
1602
|
+
error: { type: "string" }
|
|
1603
|
+
},
|
|
1604
|
+
additionalProperties: false
|
|
1605
|
+
},
|
|
1606
|
+
render(_args, value) {
|
|
1607
|
+
return [{ type: "text", text: JSON.stringify(value, null, 2) }];
|
|
1608
|
+
}
|
|
1609
|
+
},
|
|
1610
|
+
async execute(args, exec) {
|
|
1611
|
+
const workdir = sessionWorkdir(exec, args, ctx);
|
|
1612
|
+
if (!shell) return { ok: false, isRepo: false, workdir: workdir || "", topLevel: "", branch: "", status: "", recentCommits: "", stashes: "", remotes: "", error: "shell \u670D\u52A1\u4E0D\u53EF\u7528" };
|
|
1613
|
+
const command = "echo '__TOP__'; git rev-parse --show-toplevel 2>&1; echo '__BRANCH__'; git branch --show-current 2>&1; echo '__STATUS__'; git status --short --branch 2>&1; echo '__LOG__'; git log --oneline -8 2>&1; echo '__STASH__'; git stash list 2>&1; echo '__REMOTE__'; git remote -v 2>&1";
|
|
1614
|
+
const r = await runGit(shell, workdir, command, 2e4, 3e4, exec.signal);
|
|
1615
|
+
const text = (r.stdout?.text ?? "") + (r.stderr?.text ?? "");
|
|
1616
|
+
const keys = ["__TOP__", "__BRANCH__", "__STATUS__", "__LOG__", "__STASH__", "__REMOTE__"];
|
|
1617
|
+
const parts = {};
|
|
1618
|
+
let idx = 0;
|
|
1619
|
+
for (const [k, key] of keys.entries()) {
|
|
1620
|
+
const start = text.indexOf(key, idx);
|
|
1621
|
+
if (start < 0) {
|
|
1622
|
+
parts[key] = "";
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
const valueStart = start + key.length;
|
|
1626
|
+
const nextKey = keys[k + 1];
|
|
1627
|
+
const end = nextKey ? text.indexOf(nextKey, valueStart) : text.length;
|
|
1628
|
+
parts[key] = end < 0 ? text.slice(valueStart) : text.slice(valueStart, end);
|
|
1629
|
+
idx = end < 0 ? text.length : end;
|
|
1630
|
+
}
|
|
1631
|
+
const top = (parts.__TOP__ ?? "").trim();
|
|
1632
|
+
const isRepo = /^\/|^[A-Za-z]:[\\/]/.test(top);
|
|
1633
|
+
return {
|
|
1634
|
+
ok: true,
|
|
1635
|
+
isRepo,
|
|
1636
|
+
workdir: workdir || "",
|
|
1637
|
+
topLevel: top,
|
|
1638
|
+
branch: redactAndLimit((parts.__BRANCH__ ?? "").trim(), 4096),
|
|
1639
|
+
status: redactAndLimit((parts.__STATUS__ ?? "").trim()),
|
|
1640
|
+
recentCommits: redactAndLimit((parts.__LOG__ ?? "").trim()),
|
|
1641
|
+
stashes: redactAndLimit((parts.__STASH__ ?? "").trim()),
|
|
1642
|
+
remotes: redactAndLimit((parts.__REMOTE__ ?? "").trim()),
|
|
1643
|
+
error: ""
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
const registerWebServer = (webServer) => registerGitGuideActions(webServer, {
|
|
1649
|
+
repository,
|
|
1650
|
+
proposalStorageReady,
|
|
1651
|
+
shell,
|
|
1652
|
+
repositoryContext: (sessionId) => repositoryContextForSession(ctx, sandboxPolicy, sessionId),
|
|
1653
|
+
latestPending,
|
|
1654
|
+
findProposal,
|
|
1655
|
+
proposalView,
|
|
1656
|
+
flushProposal: (sessionId) => proposalService.flush(sessionId),
|
|
1657
|
+
captureFingerprint,
|
|
1658
|
+
runChecks,
|
|
1659
|
+
verifyProposal,
|
|
1660
|
+
executeProposal: (activeShell, proposal, policy, persist) => executeRegisteredProposal(activeShell, proposal, void 0, policy, persist),
|
|
1661
|
+
resolveExecutionPolicy: (sessionId) => {
|
|
1662
|
+
const agents = ctx.get("agents");
|
|
1663
|
+
const agent = agents ? agents.get(sessionId) : void 0;
|
|
1664
|
+
return sandboxPolicy ? sandboxPolicy.resolve({ session: agent?.session }) : void 0;
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
if (typeof ctx.inject === "function") {
|
|
1668
|
+
ctx.inject(["webServer"], (webCtx) => registerWebServer(webCtx.get("webServer")));
|
|
1669
|
+
} else {
|
|
1670
|
+
registerWebServer(ctx.get("webServer"));
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
var helpers = {
|
|
1675
|
+
parseCommand,
|
|
1676
|
+
validateCommand,
|
|
1677
|
+
modernizeCommand,
|
|
1678
|
+
classifyRisk,
|
|
1679
|
+
classifyStepsRisk,
|
|
1680
|
+
quoteShellArg,
|
|
1681
|
+
redactSecrets,
|
|
1682
|
+
redactAndLimit,
|
|
1683
|
+
addPathsOf,
|
|
1684
|
+
deriveChecks,
|
|
1685
|
+
runChecks,
|
|
1686
|
+
verifyProposal,
|
|
1687
|
+
captureFingerprint,
|
|
1688
|
+
captureDiagnostics,
|
|
1689
|
+
executeProposalSteps,
|
|
1690
|
+
executeRegisteredProposal,
|
|
1691
|
+
buildRecovery,
|
|
1692
|
+
registerRecoveryProposal,
|
|
1693
|
+
storeProposal,
|
|
1694
|
+
findProposal,
|
|
1695
|
+
proposalView,
|
|
1696
|
+
latestPending,
|
|
1697
|
+
ProposalService
|
|
1698
|
+
};
|
|
1699
|
+
module2.exports = Object.assign(plugin2, { helpers });
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
// src/host/index.ts
|
|
1704
|
+
var plugin = require_plugin();
|
|
1705
|
+
module.exports = plugin;
|