@xbzbing/dsh-git-panel 1.5.3 → 1.5.4
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/{README_EN.md → README.en.md} +2 -2
- package/README.md +3 -3
- package/lib/host/actions.d.ts +3 -3
- package/lib/host/actions.js +230 -0
- package/lib/host/core.d.ts +3 -3
- package/lib/host/core.js +300 -0
- package/lib/host/git.js +65 -0
- package/lib/host/index.d.ts +10 -10
- package/lib/host/index.js +27 -1680
- package/lib/host/llm-face.js +0 -0
- package/lib/host/parser.d.ts +1 -1
- package/lib/host/parser.js +213 -0
- package/lib/host/queries.d.ts +2 -2
- package/lib/host/queries.js +461 -0
- package/lib/host/remote.d.ts +1 -1
- package/lib/host/remote.js +48 -0
- package/lib/host/suggest.d.ts +2 -2
- package/lib/host/suggest.js +217 -0
- package/lib/host/types.js +22 -0
- package/lib/host/validate.js +26 -0
- package/lib/host/version.js +130 -0
- package/package.json +10 -3
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { mapWorkspaceFailure, resolveWorkspace, runCommand } from "./core.js";
|
|
2
|
+
import { isSafePath } from "./validate.js";
|
|
3
|
+
import { parseStatus } from "./parser.js";
|
|
4
|
+
const SYSTEM_PROMPT = [
|
|
5
|
+
"You are an expert at writing concise, accurate git commit messages.",
|
|
6
|
+
"Given a repository\u2019s uncommitted changes, write ONE commit message for them.",
|
|
7
|
+
"The message must start with a short imperative subject line (72 chars max), optionally followed by a blank line and a brief body explaining what changed and why.",
|
|
8
|
+
"Return only the message text: no quotes, code fences, prefixes, bullet lists, or explanations.",
|
|
9
|
+
"Match the language and style of the recent commit message when one is provided; otherwise write in English.",
|
|
10
|
+
"Focus on the most important change; do not mechanically list every file."
|
|
11
|
+
].join("\n");
|
|
12
|
+
const UNTRACKED_FILE_CAP = 16;
|
|
13
|
+
function resolveRoute(config, agentDefaultModel) {
|
|
14
|
+
if (config.suggestProvider !== void 0 && config.suggestModel !== void 0) {
|
|
15
|
+
return { provider: config.suggestProvider, model: config.suggestModel };
|
|
16
|
+
}
|
|
17
|
+
return agentDefaultModel?.currentSelection();
|
|
18
|
+
}
|
|
19
|
+
function withDeadline(timeoutMs, parent) {
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
let timedOut = false;
|
|
22
|
+
const timer = setTimeout(() => {
|
|
23
|
+
timedOut = true;
|
|
24
|
+
controller.abort();
|
|
25
|
+
}, timeoutMs);
|
|
26
|
+
const onAbort = () => {
|
|
27
|
+
controller.abort();
|
|
28
|
+
};
|
|
29
|
+
if (parent !== void 0) {
|
|
30
|
+
if (parent.aborted) {
|
|
31
|
+
controller.abort();
|
|
32
|
+
} else parent.addEventListener("abort", onAbort, { once: true });
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
signal: controller.signal,
|
|
36
|
+
expired: () => timedOut,
|
|
37
|
+
dispose: () => {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
parent?.removeEventListener("abort", onAbort);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function truncateUtf8(text, maxBytes) {
|
|
44
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
45
|
+
let lo = 0;
|
|
46
|
+
let hi = text.length;
|
|
47
|
+
while (lo < hi) {
|
|
48
|
+
const mid = lo + hi + 1 >> 1;
|
|
49
|
+
if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes) lo = mid;
|
|
50
|
+
else hi = mid - 1;
|
|
51
|
+
}
|
|
52
|
+
return text.slice(0, lo);
|
|
53
|
+
}
|
|
54
|
+
function frameChanges(files, diff, recentCommitMessage) {
|
|
55
|
+
return JSON.stringify({
|
|
56
|
+
files,
|
|
57
|
+
diff,
|
|
58
|
+
recentCommitMessage: recentCommitMessage === "" ? null : recentCommitMessage
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function normalizeMessage(raw) {
|
|
62
|
+
let m = raw.trim();
|
|
63
|
+
if (m.startsWith("```")) {
|
|
64
|
+
const firstNl = m.indexOf("\n");
|
|
65
|
+
if (firstNl !== -1) {
|
|
66
|
+
m = m.slice(firstNl + 1);
|
|
67
|
+
m = m.endsWith("```") ? m.slice(0, -3) : m;
|
|
68
|
+
m = m.trim();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return m.replace(/\n---+\s*$/, "").trim();
|
|
72
|
+
}
|
|
73
|
+
function finishError(reason) {
|
|
74
|
+
if (reason === void 0 || reason.kind === "stop") return void 0;
|
|
75
|
+
if (reason.kind === "max-tokens") return { ok: false, error: { code: "llm-output", message: "model output hit the token limit" } };
|
|
76
|
+
if (reason.kind === "tool-calls") return { ok: false, error: { code: "llm-output", message: "model requested a tool instead of writing a message" } };
|
|
77
|
+
const failure = "failure" in reason ? reason.failure : void 0;
|
|
78
|
+
return { ok: false, error: { code: "llm-error", message: failure?.message ?? `model stream finished abnormally (${reason.kind})` } };
|
|
79
|
+
}
|
|
80
|
+
async function collectDiff(deps, root, hasHead, pathArgs) {
|
|
81
|
+
const cmds = hasHead ? [["git", "diff", "HEAD", ...pathArgs]] : [["git", "diff", "--cached", ...pathArgs], ["git", "diff", ...pathArgs]];
|
|
82
|
+
const runs = await Promise.all(cmds.map((argv) => runCommand(deps.run, argv, root, "suggest-diff", deps.signal)));
|
|
83
|
+
let diff = "";
|
|
84
|
+
for (const res of runs) {
|
|
85
|
+
if ("failure" in res) return { ok: false, error: { code: "git-unavailable", message: "diff failed" } };
|
|
86
|
+
if (res.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
87
|
+
if (res.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
88
|
+
if (res.run.exitCode !== 0) {
|
|
89
|
+
return { ok: false, error: { code: "git-error", message: res.run.stderr.trim() || "git diff failed" } };
|
|
90
|
+
}
|
|
91
|
+
diff += res.run.stdout;
|
|
92
|
+
}
|
|
93
|
+
return diff;
|
|
94
|
+
}
|
|
95
|
+
async function appendUntracked(deps, root, files, diff, budget) {
|
|
96
|
+
let text = diff;
|
|
97
|
+
let truncated = false;
|
|
98
|
+
const candidates = files.filter((c) => c.status === "untracked" && !c.isDirectory && isSafePath(c.path));
|
|
99
|
+
for (const c of candidates.slice(0, UNTRACKED_FILE_CAP)) {
|
|
100
|
+
const remaining = budget - Buffer.byteLength(text, "utf8");
|
|
101
|
+
if (remaining <= 0) {
|
|
102
|
+
truncated = true;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
const res = await runCommand(deps.run, ["git", "diff", "--no-index", "--", "/dev/null", c.path], root, "suggest-untracked", deps.signal);
|
|
106
|
+
if ("failure" in res || res.run.cancelled || res.run.timedOut) continue;
|
|
107
|
+
const chunk = res.run.stdout;
|
|
108
|
+
if (chunk === "") continue;
|
|
109
|
+
if (Buffer.byteLength(chunk, "utf8") > remaining) {
|
|
110
|
+
text += truncateUtf8(chunk, remaining) + "\n... [diff truncated]";
|
|
111
|
+
truncated = true;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
text += chunk;
|
|
115
|
+
}
|
|
116
|
+
return { diff: text, truncated };
|
|
117
|
+
}
|
|
118
|
+
async function runSuggest(deps, config, request) {
|
|
119
|
+
if (!config.suggestEnabled) {
|
|
120
|
+
return { ok: false, error: { code: "suggest-disabled", message: "AI suggest is turned off in the plugin config" } };
|
|
121
|
+
}
|
|
122
|
+
const workspace = await resolveWorkspace(deps, request.sessionId);
|
|
123
|
+
if (!workspace.ok) return { ok: false, error: mapWorkspaceFailure(workspace.failure) };
|
|
124
|
+
const root = workspace.root;
|
|
125
|
+
const paths = request.paths ?? [];
|
|
126
|
+
for (const path of paths) {
|
|
127
|
+
if (!isSafePath(path)) return { ok: false, error: { code: "invalid-path", message: path } };
|
|
128
|
+
}
|
|
129
|
+
const pathArgs = paths.length > 0 ? ["--", ...paths] : [];
|
|
130
|
+
const pathSet = new Set(paths);
|
|
131
|
+
const [statusRes, headRes, lastRes] = await Promise.all([
|
|
132
|
+
runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z"], root, "suggest-status", deps.signal),
|
|
133
|
+
runCommand(deps.run, ["git", "rev-parse", "--verify", "HEAD"], root, "suggest-head", deps.signal),
|
|
134
|
+
runCommand(deps.run, ["git", "log", "-1", "--format=%B"], root, "suggest-last", deps.signal)
|
|
135
|
+
]);
|
|
136
|
+
if ("failure" in statusRes) return { ok: false, error: { code: "git-unavailable", message: "status failed" } };
|
|
137
|
+
if (statusRes.run.cancelled) return { ok: false, error: { code: "cancelled" } };
|
|
138
|
+
if (statusRes.run.timedOut) return { ok: false, error: { code: "timeout" } };
|
|
139
|
+
if (statusRes.run.exitCode !== 0) {
|
|
140
|
+
return { ok: false, error: { code: "git-error", message: statusRes.run.stderr.trim() || "git status failed" } };
|
|
141
|
+
}
|
|
142
|
+
const hasHead = "run" in headRes && headRes.run.exitCode === 0;
|
|
143
|
+
const recentCommitMessage = "run" in lastRes && lastRes.run.exitCode === 0 ? lastRes.run.stdout.replace(/\n+$/, "") : "";
|
|
144
|
+
const all = statusRes.run.stdoutLossy ? [] : parseStatus(statusRes.run.stdout);
|
|
145
|
+
const files = pathSet.size > 0 ? all.filter((c) => pathSet.has(c.path)) : all;
|
|
146
|
+
const collected = await collectDiff(deps, root, hasHead, pathArgs);
|
|
147
|
+
if (typeof collected !== "string") return collected;
|
|
148
|
+
if (files.length === 0 && collected.trim() === "") {
|
|
149
|
+
return { ok: false, error: { code: "empty-diff", message: "no uncommitted changes to summarize" } };
|
|
150
|
+
}
|
|
151
|
+
let diff = collected;
|
|
152
|
+
let truncated = false;
|
|
153
|
+
if (Buffer.byteLength(diff, "utf8") > config.suggestMaxBytes) {
|
|
154
|
+
diff = truncateUtf8(diff, config.suggestMaxBytes) + "\n... [diff truncated]";
|
|
155
|
+
truncated = true;
|
|
156
|
+
}
|
|
157
|
+
const untracked = await appendUntracked(deps, root, files, diff, config.suggestMaxBytes);
|
|
158
|
+
diff = untracked.diff;
|
|
159
|
+
truncated = truncated || untracked.truncated;
|
|
160
|
+
const llm = deps.getLlm?.();
|
|
161
|
+
const route = resolveRoute(config, deps.getAgentDefaultModel?.());
|
|
162
|
+
if (llm === void 0 || route === void 0) {
|
|
163
|
+
return { ok: false, error: { code: "llm-unavailable", message: "no model service or route is configured" } };
|
|
164
|
+
}
|
|
165
|
+
const frame = frameChanges(
|
|
166
|
+
files.map((c) => ({ path: c.path, status: c.status })),
|
|
167
|
+
diff,
|
|
168
|
+
recentCommitMessage
|
|
169
|
+
);
|
|
170
|
+
const deadline = withDeadline(config.suggestTimeoutMs, deps.signal);
|
|
171
|
+
const options = {
|
|
172
|
+
provider: route.provider,
|
|
173
|
+
model: route.model,
|
|
174
|
+
system: SYSTEM_PROMPT,
|
|
175
|
+
messages: [{
|
|
176
|
+
id: "git-panel-suggest",
|
|
177
|
+
role: "user",
|
|
178
|
+
content: [{ type: "text", text: frame }],
|
|
179
|
+
source: { kind: "user" }
|
|
180
|
+
}],
|
|
181
|
+
maxTokens: config.suggestMaxOutputTokens,
|
|
182
|
+
temperature: 0.3,
|
|
183
|
+
signal: deadline.signal
|
|
184
|
+
};
|
|
185
|
+
let output = "";
|
|
186
|
+
let finish;
|
|
187
|
+
try {
|
|
188
|
+
for await (const chunk of llm.stream(options)) {
|
|
189
|
+
if (chunk.type === "text-delta") output += chunk.text;
|
|
190
|
+
else if (chunk.type === "finish") finish = chunk.reason;
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (deps.signal?.aborted === true) return { ok: false, error: { code: "cancelled" } };
|
|
194
|
+
if (deadline.expired()) return { ok: false, error: { code: "llm-error", message: `model call timed out after ${config.suggestTimeoutMs}ms` } };
|
|
195
|
+
return { ok: false, error: { code: "llm-error", message: error instanceof Error ? error.message : "model stream failed" } };
|
|
196
|
+
} finally {
|
|
197
|
+
deadline.dispose();
|
|
198
|
+
}
|
|
199
|
+
if (deps.signal?.aborted === true) return { ok: false, error: { code: "cancelled" } };
|
|
200
|
+
if (deadline.expired()) return { ok: false, error: { code: "llm-error", message: `model call timed out after ${config.suggestTimeoutMs}ms` } };
|
|
201
|
+
const terminal = finishError(finish);
|
|
202
|
+
if (terminal !== void 0) return terminal;
|
|
203
|
+
const message = normalizeMessage(output);
|
|
204
|
+
if (message === "") return { ok: false, error: { code: "llm-output", message: "model produced no message text" } };
|
|
205
|
+
return {
|
|
206
|
+
ok: true,
|
|
207
|
+
value: {
|
|
208
|
+
message,
|
|
209
|
+
provider: route.provider,
|
|
210
|
+
model: route.model,
|
|
211
|
+
...truncated ? { truncated: true } : {}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
export {
|
|
216
|
+
runSuggest
|
|
217
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const IMAGE_MIME = {
|
|
2
|
+
png: "image/png",
|
|
3
|
+
jpg: "image/jpeg",
|
|
4
|
+
jpeg: "image/jpeg",
|
|
5
|
+
gif: "image/gif",
|
|
6
|
+
webp: "image/webp",
|
|
7
|
+
avif: "image/avif",
|
|
8
|
+
bmp: "image/bmp",
|
|
9
|
+
svg: "image/svg+xml",
|
|
10
|
+
ico: "image/x-icon",
|
|
11
|
+
tif: "image/tiff",
|
|
12
|
+
tiff: "image/tiff"
|
|
13
|
+
};
|
|
14
|
+
function imageMimeFor(path) {
|
|
15
|
+
const dot = path.lastIndexOf(".");
|
|
16
|
+
if (dot < 0) return null;
|
|
17
|
+
return IMAGE_MIME[path.slice(dot + 1).toLowerCase()] ?? null;
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
IMAGE_MIME,
|
|
21
|
+
imageMimeFor
|
|
22
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isAbsolute, normalize } from "node:path";
|
|
2
|
+
function isSafePath(path) {
|
|
3
|
+
if (path === "" || isAbsolute(path)) return false;
|
|
4
|
+
const norm = normalize(path);
|
|
5
|
+
if (norm === ".." || norm.startsWith("../") || norm.startsWith("..\\")) return false;
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
const REV_META = /[\x00-\x20\x7f~^:?*[\\]/;
|
|
9
|
+
function isSafeRev(input) {
|
|
10
|
+
if (input === "" || input.startsWith("-")) return false;
|
|
11
|
+
if (REV_META.test(input)) return false;
|
|
12
|
+
if (input.includes("..") || input.includes("@{")) return false;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
function isSafeBranchName(name) {
|
|
16
|
+
return isSafeRev(name) && !name.startsWith("/");
|
|
17
|
+
}
|
|
18
|
+
function isSafeRemoteName(name) {
|
|
19
|
+
return /^[A-Za-z0-9._][A-Za-z0-9._/-]*$/.test(name) && name.length <= 128;
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
isSafeBranchName,
|
|
23
|
+
isSafePath,
|
|
24
|
+
isSafeRemoteName,
|
|
25
|
+
isSafeRev
|
|
26
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
function manifestPath() {
|
|
5
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
6
|
+
}
|
|
7
|
+
function stringField(value) {
|
|
8
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
9
|
+
}
|
|
10
|
+
function parseRepository(repository) {
|
|
11
|
+
const raw = typeof repository === "string" ? repository : typeof repository === "object" && repository !== null ? stringField(repository.url) : void 0;
|
|
12
|
+
if (raw === void 0) return void 0;
|
|
13
|
+
const fromHost = raw.match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?(?:$|[/#?])/i);
|
|
14
|
+
if (fromHost !== null) return { owner: fromHost[1], repo: fromHost[2] };
|
|
15
|
+
const shorthand = raw.match(/^(?:github:)?([\w.-]+)\/([\w.-]+?)(?:\.git)?$/i);
|
|
16
|
+
if (shorthand !== null) return { owner: shorthand[1], repo: shorthand[2] };
|
|
17
|
+
return void 0;
|
|
18
|
+
}
|
|
19
|
+
function compareVersions(a, b) {
|
|
20
|
+
const split = (value) => {
|
|
21
|
+
const trimmed = value.trim().replace(/^v/i, "");
|
|
22
|
+
const [core, ...rest] = trimmed.split("-");
|
|
23
|
+
const preRaw = rest.join("-");
|
|
24
|
+
return {
|
|
25
|
+
core: core.split(".").map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part)),
|
|
26
|
+
pre: preRaw === "" ? [] : preRaw.split(".")
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const left = split(a);
|
|
30
|
+
const right = split(b);
|
|
31
|
+
const length = Math.max(left.core.length, right.core.length);
|
|
32
|
+
for (let index = 0; index < length; index += 1) {
|
|
33
|
+
const l = left.core[index] ?? 0;
|
|
34
|
+
const r = right.core[index] ?? 0;
|
|
35
|
+
if (l > r) return 1;
|
|
36
|
+
if (l < r) return -1;
|
|
37
|
+
}
|
|
38
|
+
if (left.pre.length === 0 && right.pre.length === 0) return 0;
|
|
39
|
+
if (left.pre.length === 0) return 1;
|
|
40
|
+
if (right.pre.length === 0) return -1;
|
|
41
|
+
const preLen = Math.max(left.pre.length, right.pre.length);
|
|
42
|
+
for (let i = 0; i < preLen; i += 1) {
|
|
43
|
+
const lp = left.pre[i];
|
|
44
|
+
const rp = right.pre[i];
|
|
45
|
+
if (lp === void 0) return -1;
|
|
46
|
+
if (rp === void 0) return 1;
|
|
47
|
+
const ln = Number.parseInt(lp, 10);
|
|
48
|
+
const rn = Number.parseInt(rp, 10);
|
|
49
|
+
const lNum = Number.isFinite(ln) && String(ln) === lp;
|
|
50
|
+
const rNum = Number.isFinite(rn) && String(rn) === rp;
|
|
51
|
+
if (lNum && rNum) {
|
|
52
|
+
if (ln !== rn) return ln > rn ? 1 : -1;
|
|
53
|
+
} else if (lp !== rp) return lp > rp ? 1 : -1;
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
let manifestCache;
|
|
58
|
+
async function readManifest(path) {
|
|
59
|
+
const raw = await readFile(path, "utf8");
|
|
60
|
+
const parsed = JSON.parse(raw);
|
|
61
|
+
if (typeof parsed !== "object" || parsed === null) throw new Error("package.json is not an object");
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
async function loadManifest(path) {
|
|
65
|
+
if (path !== void 0) return readManifest(path);
|
|
66
|
+
if (manifestCache === void 0) manifestCache = await readManifest(manifestPath());
|
|
67
|
+
return manifestCache;
|
|
68
|
+
}
|
|
69
|
+
async function readVersionInfo(options = {}) {
|
|
70
|
+
const manifest = await loadManifest(options.manifestPath);
|
|
71
|
+
const current = stringField(manifest.version) ?? "0.0.0";
|
|
72
|
+
const repo = parseRepository(manifest.repository);
|
|
73
|
+
const repositoryUrl = repo === void 0 ? void 0 : `https://github.com/${repo.owner}/${repo.repo}`;
|
|
74
|
+
return {
|
|
75
|
+
current,
|
|
76
|
+
...repositoryUrl ? { repositoryUrl } : {},
|
|
77
|
+
updateAvailable: false,
|
|
78
|
+
checkedRemote: false
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function safeReleaseUrl(url) {
|
|
82
|
+
if (url === void 0) return void 0;
|
|
83
|
+
return /^https:\/\/github\.com\//i.test(url) ? url : void 0;
|
|
84
|
+
}
|
|
85
|
+
const REMOTE_CACHE_MS = 10 * 60 * 1e3;
|
|
86
|
+
let remoteCache;
|
|
87
|
+
async function checkLatestVersion(options = {}) {
|
|
88
|
+
if (options.manifestPath === void 0 && options.fetchFn === void 0 && remoteCache !== void 0 && Date.now() - remoteCache.at < REMOTE_CACHE_MS) {
|
|
89
|
+
return remoteCache.info;
|
|
90
|
+
}
|
|
91
|
+
const base = await readVersionInfo({ manifestPath: options.manifestPath });
|
|
92
|
+
const repo = parseRepository((await loadManifest(options.manifestPath)).repository);
|
|
93
|
+
if (repo === void 0) {
|
|
94
|
+
return { ...base, checkedRemote: false, error: "repository is not configured" };
|
|
95
|
+
}
|
|
96
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetchFn(`https://api.github.com/repos/${repo.owner}/${repo.repo}/releases/latest`, {
|
|
99
|
+
headers: { accept: "application/vnd.github+json", "user-agent": "dsh-git-panel" },
|
|
100
|
+
signal: AbortSignal.timeout(5e3)
|
|
101
|
+
});
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
return { ...base, checkedRemote: true, error: `GitHub responded with ${response.status}` };
|
|
104
|
+
}
|
|
105
|
+
const body = await response.json();
|
|
106
|
+
const tag = typeof body === "object" && body !== null ? stringField(body.tag_name) : void 0;
|
|
107
|
+
const htmlUrl = typeof body === "object" && body !== null ? stringField(body.html_url) : void 0;
|
|
108
|
+
if (tag === void 0) {
|
|
109
|
+
return { ...base, checkedRemote: true, error: "GitHub response did not include a release tag" };
|
|
110
|
+
}
|
|
111
|
+
const latest = tag.replace(/^v/i, "");
|
|
112
|
+
const info = {
|
|
113
|
+
...base,
|
|
114
|
+
checkedRemote: true,
|
|
115
|
+
latest,
|
|
116
|
+
updateAvailable: compareVersions(latest, base.current) > 0,
|
|
117
|
+
releaseUrl: safeReleaseUrl(htmlUrl) ?? (base.repositoryUrl !== void 0 ? `${base.repositoryUrl}/releases/latest` : void 0)
|
|
118
|
+
};
|
|
119
|
+
if (options.manifestPath === void 0 && options.fetchFn === void 0) remoteCache = { at: Date.now(), info };
|
|
120
|
+
return info;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return { ...base, checkedRemote: true, error: error instanceof Error ? error.message : "Unable to reach GitHub" };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export {
|
|
126
|
+
checkLatestVersion,
|
|
127
|
+
compareVersions,
|
|
128
|
+
parseRepository,
|
|
129
|
+
readVersionInfo
|
|
130
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xbzbing/dsh-git-panel",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.4",
|
|
4
4
|
"description": "DeepSeek Harness (dsh) plugin: an IDE-style Git panel in the Web GUI — branch/history overview, uncommitted changes with commit & amend, diff stats, and a zsh-style branch marker in the input bar.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"./client": {
|
|
26
26
|
"default": "./lib/client.js"
|
|
27
27
|
},
|
|
28
|
-
"./locale
|
|
28
|
+
"./locale/zh.json": "./locale/zh.json",
|
|
29
|
+
"./locale/en.json": "./locale/en.json",
|
|
29
30
|
"./package.json": "./package.json"
|
|
30
31
|
},
|
|
31
32
|
"files": [
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
"icon.svg",
|
|
36
37
|
"cordis.patch.yml",
|
|
37
38
|
"README.md",
|
|
38
|
-
"
|
|
39
|
+
"README.en.md",
|
|
39
40
|
"LICENSE"
|
|
40
41
|
],
|
|
41
42
|
"scripts": {
|
|
@@ -58,6 +59,12 @@
|
|
|
58
59
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
59
60
|
"@deepseek-ai/dsh-client-ui-primitives"
|
|
60
61
|
]
|
|
62
|
+
},
|
|
63
|
+
"compatibility": {
|
|
64
|
+
"dsh": ">=0.1.7-rc.2",
|
|
65
|
+
"dshReleases": {
|
|
66
|
+
"0.2.1-alpha.2": "compatible"
|
|
67
|
+
}
|
|
61
68
|
}
|
|
62
69
|
},
|
|
63
70
|
"keywords": [
|