@voltage12/dsh-simple-worktree 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +104 -2
- package/cordis.patch.yml +4 -0
- package/docs/create-dialog-worktree.png +0 -0
- package/docs/create-dialog.png +0 -0
- package/docs/created-workspace.png +0 -0
- package/docs/delete-dialog.png +0 -0
- package/docs/git-missing.png +0 -0
- package/docs/non-git-toast.png +0 -0
- package/lib/client.js +752 -0
- package/lib/index.js +389 -0
- package/package.json +83 -4
package/lib/index.js
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// src/host/index.js
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
|
|
6
|
+
var name = "simple-worktree";
|
|
7
|
+
var inject = [];
|
|
8
|
+
var ROUTE_PREFIX = "/plugin/simple-worktree";
|
|
9
|
+
var BODY_LIMIT = 64 * 1024;
|
|
10
|
+
var MAX_SLOT_ATTEMPTS = 200;
|
|
11
|
+
function runGit(cwd, args) {
|
|
12
|
+
return new Promise((resolvePromise) => {
|
|
13
|
+
let child;
|
|
14
|
+
try {
|
|
15
|
+
child = spawn("git", args, { cwd, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
resolvePromise({
|
|
18
|
+
ok: false,
|
|
19
|
+
code: -1,
|
|
20
|
+
missing: error?.code === "ENOENT",
|
|
21
|
+
stdout: "",
|
|
22
|
+
stderr: String(error?.message ?? error)
|
|
23
|
+
});
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
let stdout = "";
|
|
27
|
+
let stderr = "";
|
|
28
|
+
child.stdout.setEncoding("utf8");
|
|
29
|
+
child.stderr.setEncoding("utf8");
|
|
30
|
+
child.stdout.on("data", (chunk) => {
|
|
31
|
+
stdout += chunk;
|
|
32
|
+
});
|
|
33
|
+
child.stderr.on("data", (chunk) => {
|
|
34
|
+
stderr += chunk;
|
|
35
|
+
});
|
|
36
|
+
child.on("error", (error) => {
|
|
37
|
+
resolvePromise({
|
|
38
|
+
ok: false,
|
|
39
|
+
code: -1,
|
|
40
|
+
missing: error?.code === "ENOENT",
|
|
41
|
+
stdout,
|
|
42
|
+
stderr: String(error?.message ?? error)
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
child.on("close", (code) => {
|
|
46
|
+
resolvePromise({ ok: code === 0, code: code ?? -1, stdout, stderr });
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function samePath(left, right) {
|
|
51
|
+
const a = resolve(left);
|
|
52
|
+
const b = resolve(right);
|
|
53
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
54
|
+
}
|
|
55
|
+
function firstLine(text) {
|
|
56
|
+
return String(text ?? "").split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0) ?? "";
|
|
57
|
+
}
|
|
58
|
+
function gitMessage(result, fallback) {
|
|
59
|
+
const line = firstLine(result.stderr);
|
|
60
|
+
return line.length > 0 ? line.replace(/^(fatal|error):\s*/i, "") : fallback;
|
|
61
|
+
}
|
|
62
|
+
function friendlyGitMessage(result, fallback) {
|
|
63
|
+
const raw = String(result?.stderr ?? "");
|
|
64
|
+
if (/already checked out at/i.test(raw))
|
|
65
|
+
return "\u8BE5\u5206\u652F\u5DF2\u88AB\u5176\u4ED6 worktree \u68C0\u51FA";
|
|
66
|
+
if (/is a missing but already registered worktree/i.test(raw))
|
|
67
|
+
return "\u8BE5\u76EE\u5F55\u5DF2\u88AB git \u767B\u8BB0\u4E3A worktree\uFF0C\u8BF7\u5148\u6267\u884C git worktree prune";
|
|
68
|
+
if (/contains modified or untracked files/i.test(raw))
|
|
69
|
+
return "worktree \u5185\u6709\u672A\u63D0\u4EA4\u7684\u6539\u52A8\uFF0C\u65E0\u6CD5\u5220\u9664";
|
|
70
|
+
if (/is locked/i.test(raw))
|
|
71
|
+
return "worktree \u5DF2\u88AB\u9501\u5B9A\uFF0C\u65E0\u6CD5\u5220\u9664";
|
|
72
|
+
if (/invalid reference/i.test(raw))
|
|
73
|
+
return "\u5206\u652F\u6216\u63D0\u4EA4\u4E0D\u5B58\u5728";
|
|
74
|
+
if (/not a git repository/i.test(raw))
|
|
75
|
+
return "\u5F53\u524D\u76EE\u5F55\u4E0D\u662F Git \u4ED3\u5E93";
|
|
76
|
+
if (/permission denied/i.test(raw))
|
|
77
|
+
return "\u6CA1\u6709\u6743\u9650\u8BBF\u95EE\u8BE5\u76EE\u5F55";
|
|
78
|
+
const line = firstLine(raw);
|
|
79
|
+
return line.length > 0 ? `${fallback}\uFF1A${line}` : fallback;
|
|
80
|
+
}
|
|
81
|
+
async function requireDirectory(value) {
|
|
82
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
83
|
+
throw new Error("\u7F3A\u5C11\u76EE\u5F55\u53C2\u6570");
|
|
84
|
+
if (!isAbsolute(value))
|
|
85
|
+
throw new Error(`\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\uFF1A${value}`);
|
|
86
|
+
let info;
|
|
87
|
+
try {
|
|
88
|
+
info = await stat(value);
|
|
89
|
+
} catch {
|
|
90
|
+
throw new Error(`\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${value}`);
|
|
91
|
+
}
|
|
92
|
+
if (!info.isDirectory())
|
|
93
|
+
throw new Error(`\u4E0D\u662F\u76EE\u5F55\uFF1A${value}`);
|
|
94
|
+
return resolve(value);
|
|
95
|
+
}
|
|
96
|
+
async function checkedOutBranches(top) {
|
|
97
|
+
const result = await runGit(top, ["worktree", "list", "--porcelain"]);
|
|
98
|
+
const map = /* @__PURE__ */ new Map();
|
|
99
|
+
if (!result.ok)
|
|
100
|
+
return map;
|
|
101
|
+
let current = "";
|
|
102
|
+
for (const raw of result.stdout.split(/\r?\n/)) {
|
|
103
|
+
if (raw.startsWith("worktree ")) {
|
|
104
|
+
current = raw.slice("worktree ".length);
|
|
105
|
+
} else if (raw.startsWith("branch refs/heads/")) {
|
|
106
|
+
map.set(raw.slice("branch refs/heads/".length), current);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return map;
|
|
110
|
+
}
|
|
111
|
+
async function localBranches(top) {
|
|
112
|
+
const result = await runGit(top, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]);
|
|
113
|
+
if (!result.ok)
|
|
114
|
+
return [];
|
|
115
|
+
return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
116
|
+
}
|
|
117
|
+
async function probe(directory) {
|
|
118
|
+
const dir = await requireDirectory(directory);
|
|
119
|
+
const inside = await runGit(dir, ["rev-parse", "--is-inside-work-tree"]);
|
|
120
|
+
if (!inside.ok) {
|
|
121
|
+
if (inside.missing === true)
|
|
122
|
+
return { isRepo: false, gitMissing: true, error: "\u672A\u627E\u5230 git \u547D\u4EE4\uFF0C\u8BF7\u5148\u5B89\u88C5 Git \u5E76\u52A0\u5165 PATH" };
|
|
123
|
+
return {
|
|
124
|
+
isRepo: false,
|
|
125
|
+
error: /not a git repository/i.test(String(inside.stderr)) ? "\u5F53\u524D\u76EE\u5F55\u4E0D\u662F Git \u4ED3\u5E93" : gitMessage(inside, "\u65E0\u6CD5\u8BFB\u53D6 Git \u4ED3\u5E93\u4FE1\u606F")
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const topResult = await runGit(dir, ["rev-parse", "--show-toplevel"]);
|
|
129
|
+
if (!topResult.ok)
|
|
130
|
+
return { isRepo: false, error: gitMessage(topResult, "\u65E0\u6CD5\u89E3\u6790\u4ED3\u5E93\u6839\u76EE\u5F55") };
|
|
131
|
+
const top = normalize(topResult.stdout.trim());
|
|
132
|
+
const gitDirResult = await runGit(dir, ["rev-parse", "--path-format=absolute", "--git-dir"]);
|
|
133
|
+
const commonDirResult = await runGit(dir, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
134
|
+
let gitDir = gitDirResult.ok ? gitDirResult.stdout.trim() : "";
|
|
135
|
+
let commonDir = commonDirResult.ok ? commonDirResult.stdout.trim() : "";
|
|
136
|
+
if (gitDir === "" || commonDir === "") {
|
|
137
|
+
const legacyGit = await runGit(dir, ["rev-parse", "--git-dir"]);
|
|
138
|
+
const legacyCommon = await runGit(dir, ["rev-parse", "--git-common-dir"]);
|
|
139
|
+
gitDir = legacyGit.ok ? legacyGit.stdout.trim() : gitDir;
|
|
140
|
+
commonDir = legacyCommon.ok ? legacyCommon.stdout.trim() : commonDir;
|
|
141
|
+
}
|
|
142
|
+
if (gitDir !== "" && !isAbsolute(gitDir))
|
|
143
|
+
gitDir = resolve(top, gitDir);
|
|
144
|
+
if (commonDir !== "" && !isAbsolute(commonDir))
|
|
145
|
+
commonDir = resolve(top, commonDir);
|
|
146
|
+
const isLinkedWorktree = gitDir !== "" && commonDir !== "" && !samePath(gitDir, commonDir);
|
|
147
|
+
const mainRoot = normalize(isLinkedWorktree ? dirname(commonDir) : top);
|
|
148
|
+
const branchResult = await runGit(dir, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
149
|
+
const branch = branchResult.ok ? branchResult.stdout.trim() : void 0;
|
|
150
|
+
const head = await runGit(dir, ["rev-parse", "--verify", "--quiet", "HEAD"]);
|
|
151
|
+
const outs = await checkedOutBranches(top);
|
|
152
|
+
const names = await localBranches(top);
|
|
153
|
+
const branches = names.map((branchName) => ({
|
|
154
|
+
name: branchName,
|
|
155
|
+
worktreePath: outs.get(branchName) ?? null
|
|
156
|
+
}));
|
|
157
|
+
return {
|
|
158
|
+
isRepo: true,
|
|
159
|
+
top,
|
|
160
|
+
mainRoot,
|
|
161
|
+
gitDir,
|
|
162
|
+
commonDir,
|
|
163
|
+
isLinkedWorktree,
|
|
164
|
+
branch,
|
|
165
|
+
hasCommits: head.ok,
|
|
166
|
+
branches
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function flattenBranch(branch) {
|
|
170
|
+
return branch.split("/").join("-");
|
|
171
|
+
}
|
|
172
|
+
function pickSlot(agentsDir, slug) {
|
|
173
|
+
for (let index = 1; index <= MAX_SLOT_ATTEMPTS; index += 1) {
|
|
174
|
+
const candidate = join(agentsDir, index === 1 ? slug : `${slug}-${index}`);
|
|
175
|
+
if (!existsSync(candidate))
|
|
176
|
+
return candidate;
|
|
177
|
+
}
|
|
178
|
+
throw new Error(`\u65E0\u6CD5\u4E3A ${slug} \u627E\u5230\u7A7A\u95F2\u76EE\u5F55\u540D`);
|
|
179
|
+
}
|
|
180
|
+
async function ensureGitignore(repoRoot) {
|
|
181
|
+
const file = join(repoRoot, ".gitignore");
|
|
182
|
+
let text = "";
|
|
183
|
+
try {
|
|
184
|
+
text = await readFile(file, "utf8");
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error?.code !== "ENOENT")
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
const covered = text.split(/\r?\n/).some((raw) => {
|
|
190
|
+
const line = raw.trim();
|
|
191
|
+
return line === ".agents/worktree" || line === ".agents/worktree/" || line === "/.agents/worktree" || line === "/.agents/worktree/" || line === ".agents" || line === ".agents/" || line === "/.agents" || line === "/.agents/";
|
|
192
|
+
});
|
|
193
|
+
if (covered)
|
|
194
|
+
return false;
|
|
195
|
+
const separator = text === "" || text.endsWith("\n") ? "" : "\n";
|
|
196
|
+
await writeFile(file, `${text}${separator}.agents/worktree/
|
|
197
|
+
`, "utf8");
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
async function assertBranchName(repoRoot, branch) {
|
|
201
|
+
const result = await runGit(repoRoot, ["check-ref-format", "--branch", branch]);
|
|
202
|
+
if (!result.ok)
|
|
203
|
+
throw new Error(`\u5206\u652F\u540D\u4E0D\u5408\u6CD5\uFF1A${branch}`);
|
|
204
|
+
}
|
|
205
|
+
async function createWorktree(body) {
|
|
206
|
+
const target = await probe(body?.path);
|
|
207
|
+
if (target.gitMissing === true)
|
|
208
|
+
throw new Error("\u672A\u627E\u5230 git \u547D\u4EE4\uFF0C\u8BF7\u5148\u5B89\u88C5 Git \u5E76\u52A0\u5165 PATH");
|
|
209
|
+
if (!target.isRepo)
|
|
210
|
+
throw new Error(target.error ?? "\u5F53\u524D\u76EE\u5F55\u4E0D\u662F Git \u4ED3\u5E93");
|
|
211
|
+
if (!target.hasCommits)
|
|
212
|
+
throw new Error("\u4ED3\u5E93\u8FD8\u6CA1\u6709\u4EFB\u4F55\u63D0\u4EA4\uFF0C\u65E0\u6CD5\u521B\u5EFA worktree");
|
|
213
|
+
const mode = body?.mode === "existing" ? "existing" : "new";
|
|
214
|
+
const branch = String(body?.branch ?? "").trim();
|
|
215
|
+
if (branch === "")
|
|
216
|
+
throw new Error("\u8BF7\u8F93\u5165\u5206\u652F\u540D");
|
|
217
|
+
await assertBranchName(target.mainRoot, branch);
|
|
218
|
+
const existing = target.branches.find((item) => item.name === branch);
|
|
219
|
+
if (mode === "new") {
|
|
220
|
+
if (existing !== void 0)
|
|
221
|
+
throw new Error(`\u672C\u5730\u5DF2\u5B58\u5728\u5206\u652F ${branch}`);
|
|
222
|
+
} else {
|
|
223
|
+
if (existing === void 0)
|
|
224
|
+
throw new Error(`\u672C\u5730\u4E0D\u5B58\u5728\u5206\u652F ${branch}`);
|
|
225
|
+
if (existing.worktreePath !== null && existing.worktreePath !== "")
|
|
226
|
+
throw new Error(`\u5206\u652F ${branch} \u5DF2\u88AB worktree \u5360\u7528\uFF1A${existing.worktreePath}`);
|
|
227
|
+
}
|
|
228
|
+
let base = "HEAD";
|
|
229
|
+
if (mode === "new") {
|
|
230
|
+
const requested = String(body?.base ?? "").trim();
|
|
231
|
+
base = requested === "" ? target.branch ?? "HEAD" : requested;
|
|
232
|
+
if (base !== "HEAD" && !target.branches.some((item) => item.name === base))
|
|
233
|
+
throw new Error(`\u8D77\u70B9\u5206\u652F\u4E0D\u5B58\u5728\uFF1A${base}`);
|
|
234
|
+
}
|
|
235
|
+
const agentsDir = join(target.mainRoot, ".agents", "worktree");
|
|
236
|
+
await mkdir(agentsDir, { recursive: true });
|
|
237
|
+
await ensureGitignore(target.mainRoot);
|
|
238
|
+
const directory = pickSlot(agentsDir, flattenBranch(branch));
|
|
239
|
+
await runGit(target.mainRoot, ["worktree", "prune"]);
|
|
240
|
+
const args = mode === "new" ? ["worktree", "add", "-b", branch, directory, base] : ["worktree", "add", directory, branch];
|
|
241
|
+
const result = await runGit(target.mainRoot, args);
|
|
242
|
+
if (!result.ok) {
|
|
243
|
+
if (mode === "new") {
|
|
244
|
+
await runGit(target.mainRoot, ["branch", "-D", branch]);
|
|
245
|
+
}
|
|
246
|
+
await runGit(target.mainRoot, ["worktree", "prune"]);
|
|
247
|
+
throw new Error(friendlyGitMessage(result, "\u521B\u5EFA worktree \u5931\u8D25"));
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
path: directory,
|
|
251
|
+
branch,
|
|
252
|
+
repoRoot: target.mainRoot,
|
|
253
|
+
created: true
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
async function inspectWorktree(body) {
|
|
257
|
+
const target = await probe(body?.path);
|
|
258
|
+
if (target.gitMissing === true)
|
|
259
|
+
throw new Error("\u672A\u627E\u5230 git \u547D\u4EE4\uFF0C\u8BF7\u5148\u5B89\u88C5 Git \u5E76\u52A0\u5165 PATH");
|
|
260
|
+
if (!target.isRepo)
|
|
261
|
+
throw new Error(target.error ?? "\u5F53\u524D\u76EE\u5F55\u4E0D\u662F Git \u4ED3\u5E93");
|
|
262
|
+
if (!target.isLinkedWorktree)
|
|
263
|
+
throw new Error("\u5F53\u524D\u4F1A\u8BDD\u4E0D\u5728 worktree \u4E2D");
|
|
264
|
+
const status = await runGit(target.top, ["status", "--porcelain"]);
|
|
265
|
+
const dirty = status.ok ? status.stdout.split(/\r?\n/).filter((line) => line.trim() !== "").length : 0;
|
|
266
|
+
const upstream = await runGit(target.top, ["rev-list", "--count", "@{upstream}..HEAD"]);
|
|
267
|
+
const ahead = upstream.ok ? Number.parseInt(upstream.stdout.trim(), 10) || 0 : null;
|
|
268
|
+
return {
|
|
269
|
+
path: target.top,
|
|
270
|
+
branch: target.branch ?? "",
|
|
271
|
+
dirty,
|
|
272
|
+
ahead,
|
|
273
|
+
repoRoot: target.mainRoot
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
async function removeWorktree(body) {
|
|
277
|
+
const target = await probe(body?.path);
|
|
278
|
+
if (target.gitMissing === true)
|
|
279
|
+
throw new Error("\u672A\u627E\u5230 git \u547D\u4EE4\uFF0C\u8BF7\u5148\u5B89\u88C5 Git \u5E76\u52A0\u5165 PATH");
|
|
280
|
+
if (!target.isRepo)
|
|
281
|
+
throw new Error(target.error ?? "\u5F53\u524D\u76EE\u5F55\u4E0D\u662F Git \u4ED3\u5E93");
|
|
282
|
+
if (!target.isLinkedWorktree)
|
|
283
|
+
throw new Error("\u5F53\u524D\u4F1A\u8BDD\u4E0D\u5728 worktree \u4E2D");
|
|
284
|
+
if (samePath(target.top, target.mainRoot))
|
|
285
|
+
throw new Error("\u4E0D\u80FD\u5220\u9664\u4E3B\u4ED3\u5E93\u7684\u5DE5\u4F5C\u76EE\u5F55");
|
|
286
|
+
const force = body?.force === true;
|
|
287
|
+
const result = await runGit(target.mainRoot, [
|
|
288
|
+
"worktree",
|
|
289
|
+
"remove",
|
|
290
|
+
...force ? ["--force"] : [],
|
|
291
|
+
target.top
|
|
292
|
+
]);
|
|
293
|
+
if (!result.ok) {
|
|
294
|
+
await runGit(target.mainRoot, ["worktree", "unlock", target.top]);
|
|
295
|
+
await runGit(target.mainRoot, ["worktree", "prune"]);
|
|
296
|
+
if (existsSync(target.top))
|
|
297
|
+
await rm(target.top, { recursive: true, force: true });
|
|
298
|
+
await runGit(target.mainRoot, ["worktree", "prune"]);
|
|
299
|
+
if (existsSync(target.top))
|
|
300
|
+
throw new Error(friendlyGitMessage(result, "\u5220\u9664 worktree \u5931\u8D25"));
|
|
301
|
+
}
|
|
302
|
+
if (existsSync(target.top))
|
|
303
|
+
await rm(target.top, { recursive: true, force: true });
|
|
304
|
+
await runGit(target.mainRoot, ["worktree", "prune"]);
|
|
305
|
+
return {
|
|
306
|
+
removed: true,
|
|
307
|
+
path: target.top,
|
|
308
|
+
branch: target.branch ?? ""
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function readJson(req) {
|
|
312
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
313
|
+
const chunks = [];
|
|
314
|
+
let size = 0;
|
|
315
|
+
req.on("data", (chunk) => {
|
|
316
|
+
size += chunk.length;
|
|
317
|
+
if (size > BODY_LIMIT) {
|
|
318
|
+
req.destroy();
|
|
319
|
+
rejectPromise(new Error("\u8BF7\u6C42\u4F53\u8FC7\u5927"));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
chunks.push(chunk);
|
|
323
|
+
});
|
|
324
|
+
req.on("end", () => {
|
|
325
|
+
if (chunks.length === 0) {
|
|
326
|
+
resolvePromise({});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
resolvePromise(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
331
|
+
} catch (error) {
|
|
332
|
+
rejectPromise(new Error(`\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${error?.message ?? error}`));
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
req.on("error", rejectPromise);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
function send(res, status, body) {
|
|
339
|
+
res.writeHead(status, {
|
|
340
|
+
"content-type": "application/json; charset=utf-8",
|
|
341
|
+
"cache-control": "no-store"
|
|
342
|
+
});
|
|
343
|
+
res.end(JSON.stringify(body));
|
|
344
|
+
}
|
|
345
|
+
function apply(ctx) {
|
|
346
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
347
|
+
const get = (path, handler) => {
|
|
348
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
349
|
+
kind: "exact",
|
|
350
|
+
path,
|
|
351
|
+
handler: async (req, res) => {
|
|
352
|
+
try {
|
|
353
|
+
const query = new URL(req.url ?? "/", "http://x").searchParams;
|
|
354
|
+
send(res, 200, await handler(query));
|
|
355
|
+
} catch (error) {
|
|
356
|
+
send(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}), `simple-worktree: ${path} route`);
|
|
360
|
+
};
|
|
361
|
+
const post = (path, handler) => {
|
|
362
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
363
|
+
kind: "exact",
|
|
364
|
+
path,
|
|
365
|
+
handler: async (req, res) => {
|
|
366
|
+
try {
|
|
367
|
+
send(res, 200, await handler(await readJson(req)));
|
|
368
|
+
} catch (error) {
|
|
369
|
+
send(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}), `simple-worktree: ${path} route`);
|
|
373
|
+
};
|
|
374
|
+
get(`${ROUTE_PREFIX}/probe`, (query) => probe(query.get("path") ?? ""));
|
|
375
|
+
post(`${ROUTE_PREFIX}/create`, (body) => createWorktree(body));
|
|
376
|
+
post(`${ROUTE_PREFIX}/inspect`, (body) => inspectWorktree(body));
|
|
377
|
+
post(`${ROUTE_PREFIX}/remove`, (body) => removeWorktree(body));
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
export {
|
|
381
|
+
ROUTE_PREFIX,
|
|
382
|
+
apply,
|
|
383
|
+
createWorktree,
|
|
384
|
+
inject,
|
|
385
|
+
inspectWorktree,
|
|
386
|
+
name,
|
|
387
|
+
probe,
|
|
388
|
+
removeWorktree
|
|
389
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,85 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltage12/dsh-simple-worktree",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "DSH Web plugin: create and remove git worktrees from the conversation UI",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "xiejinyang (https://github.com/voltage12)",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/voltage12/dsh-simple-worktree.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/voltage12/dsh-simple-worktree#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/voltage12/dsh-simple-worktree/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"dsh",
|
|
17
|
+
"dsh-plugin",
|
|
18
|
+
"worktree",
|
|
19
|
+
"git",
|
|
20
|
+
"deepseek-harness"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "lib/index.js",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./lib/index.js",
|
|
26
|
+
"./client": "./lib/client.js",
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"lib",
|
|
31
|
+
"cordis.patch.yml",
|
|
32
|
+
"README.md",
|
|
33
|
+
"docs"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": "^22.19.0 || >=24.0.0",
|
|
40
|
+
"dsh": ">=0.2.1-rc.2 <0.3.0"
|
|
41
|
+
},
|
|
42
|
+
"dsh": {
|
|
43
|
+
"bundle": {
|
|
44
|
+
"patch": "./cordis.patch.yml"
|
|
45
|
+
},
|
|
46
|
+
"client": {
|
|
47
|
+
"platform": "web",
|
|
48
|
+
"inject": [
|
|
49
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-workspace",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
52
|
+
"@deepseek-ai/dsh-client-ui-session"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "node build.mjs",
|
|
58
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
59
|
+
"prepack": "node build.mjs && tsc -p tsconfig.json --noEmit"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
63
|
+
"@deepseek-ai/dsh-api-remote-access": "0.2.1-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-api-remotes": "0.2.1-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-api-workspace-controller": "0.2.1-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-client-store": "0.2.1-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.2",
|
|
72
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.2",
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-client-ui-workspace": "0.2.1-rc.2",
|
|
76
|
+
"@deepseek-ai/dsh-session": "0.2.1-rc.2",
|
|
77
|
+
"@types/react": "^18.3.31",
|
|
78
|
+
"@types/react-dom": "^18.3.7",
|
|
79
|
+
"esbuild": "0.25.10",
|
|
80
|
+
"playwright-core": "1.55.1",
|
|
81
|
+
"react": "^18.3.1",
|
|
82
|
+
"react-dom": "^18.3.1",
|
|
83
|
+
"typescript": "5.9.3"
|
|
84
|
+
}
|
|
85
|
+
}
|