dsh-git-ui 0.0.2 → 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.
Files changed (47) hide show
  1. package/README.md +55 -20
  2. package/README.zh.md +55 -21
  3. package/cordis.patch.yml +1 -1
  4. package/lib/client.js +36 -35
  5. package/lib/client.js.map +4 -4
  6. package/lib/contracts/host-endpoints.d.ts +27 -0
  7. package/lib/host/actions.d.ts +22 -2
  8. package/lib/host/core.d.ts +7 -1
  9. package/lib/host/index.d.ts +25 -15
  10. package/lib/host/index.js +419 -53
  11. package/lib/host/index.js.map +4 -4
  12. package/lib/host/parser.d.ts +37 -1
  13. package/lib/host/queries.d.ts +17 -0
  14. package/lib/host/types.d.ts +131 -1
  15. package/package.json +1 -1
  16. package/src/adapters/dsh/client-adapter.ts +120 -0
  17. package/src/adapters/dsh/types/cordis.d.ts +48 -0
  18. package/src/adapters/dsh/types/typert-protocol.d.ts +81 -0
  19. package/src/adapters/dsh/types/ui-primitives.d.ts +45 -0
  20. package/src/adapters/dsh/ui-primitives.ts +16 -0
  21. package/src/client/GitCenter.tsx +1414 -149
  22. package/src/client/GitPill.tsx +282 -67
  23. package/src/client/changes-diff.ts +63 -0
  24. package/src/client/controller.ts +34 -31
  25. package/src/client/error-text.ts +21 -0
  26. package/src/client/file-tree.ts +101 -0
  27. package/src/client/git-graph.ts +188 -0
  28. package/src/client/icons.tsx +292 -0
  29. package/src/client/index.ts +38 -134
  30. package/src/client/locales.ts +124 -0
  31. package/src/client/popup-close.ts +19 -0
  32. package/src/client/remote.ts +85 -3
  33. package/src/client/select-menu.tsx +113 -0
  34. package/src/client/side-by-side.ts +150 -0
  35. package/src/client/styles.ts +1375 -86
  36. package/src/client/time-format.ts +32 -0
  37. package/src/contracts/client-platform.ts +147 -0
  38. package/src/contracts/host-endpoints.ts +58 -0
  39. package/src/contracts/plugin-activation.ts +129 -0
  40. package/src/contracts/ui-context.tsx +28 -0
  41. package/src/contracts/ui-primitives.ts +48 -0
  42. package/src/host/actions.ts +66 -15
  43. package/src/host/core.ts +9 -2
  44. package/src/host/index.ts +60 -54
  45. package/src/host/parser.ts +155 -12
  46. package/src/host/queries.ts +289 -0
  47. package/src/host/types.ts +104 -0
package/lib/host/index.js CHANGED
@@ -136,10 +136,8 @@ function parseStatusHeader(line) {
136
136
  const branch = core.split("...", 1)[0] ?? core;
137
137
  return { branch: branch === "" ? null : branch, unborn: false, ahead, behind };
138
138
  }
139
- function changeStatus(x, y) {
140
- if (x === "?" && y === "?") return "untracked";
141
- if (x === "U" || y === "U" || x !== " " && y !== " ") return "conflicted";
142
- switch (x) {
139
+ function singleStatus(code) {
140
+ switch (code) {
143
141
  case "A":
144
142
  return "added";
145
143
  case "M":
@@ -156,6 +154,9 @@ function changeStatus(x, y) {
156
154
  return "modified";
157
155
  }
158
156
  }
157
+ function isConflicted(x, y) {
158
+ return x === "U" || y === "U" || x === "A" && y === "A" || x === "D" && y === "D";
159
+ }
159
160
  function parseStatusOutput(output, maxChanges) {
160
161
  const raw = output.split(NUL);
161
162
  const segments = raw[raw.length - 1] === "" ? raw.slice(0, -1) : raw;
@@ -165,6 +166,13 @@ function parseStatusOutput(output, maxChanges) {
165
166
  let untracked = 0;
166
167
  const changes = [];
167
168
  let truncated = false;
169
+ const pushChange = (path, status, isStaged) => {
170
+ if (changes.length < maxChanges) {
171
+ changes.push({ path, status, staged: isStaged, isDirectory: path.endsWith("/") });
172
+ } else {
173
+ truncated = true;
174
+ }
175
+ };
168
176
  for (let index = 1; index < segments.length; index += 1) {
169
177
  const entry = segments[index] ?? "";
170
178
  const x = entry[0] ?? " ";
@@ -176,14 +184,19 @@ function parseStatusOutput(output, maxChanges) {
176
184
  }
177
185
  if (x === "?" && y === "?") {
178
186
  untracked += 1;
179
- } else {
180
- if (x !== " " && x !== "?") staged += 1;
181
- if (y !== " " && y !== "?") modified += 1;
187
+ pushChange(path, "untracked", false);
188
+ continue;
182
189
  }
183
- if (changes.length < maxChanges) {
184
- changes.push({ path, status: changeStatus(x, y), staged: x !== " " && x !== "?" });
190
+ if (x !== " " && x !== "?") staged += 1;
191
+ if (y !== " " && y !== "?") modified += 1;
192
+ if (isConflicted(x, y)) {
193
+ pushChange(path, "conflicted", true);
194
+ } else if (x !== " " && y !== " ") {
195
+ pushChange(path, singleStatus(x), true);
196
+ pushChange(path, singleStatus(y), false);
185
197
  } else {
186
- truncated = true;
198
+ const isStaged = x !== " ";
199
+ pushChange(path, singleStatus(isStaged ? x : y), isStaged);
187
200
  }
188
201
  }
189
202
  return {
@@ -214,6 +227,92 @@ function parseLogOutput(output) {
214
227
  }
215
228
  return commits;
216
229
  }
230
+ function parseGraphLogOutput(output, remotes = []) {
231
+ const commits = [];
232
+ for (const line of output.split("\n")) {
233
+ if (line === "") continue;
234
+ const [hash, shortHash, subject, author, dateIso, parentField, decoField] = line.split(LOG_SEP);
235
+ if (hash === void 0 || hash === "") continue;
236
+ const parents = (parentField ?? "").split(" ").filter((p) => p !== "");
237
+ commits.push({
238
+ hash,
239
+ shortHash: shortHash ?? "",
240
+ subject: subject ?? "",
241
+ author: author ?? "",
242
+ dateIso: dateIso ?? "",
243
+ parents,
244
+ refs: parseDecorations(decoField ?? "", remotes)
245
+ });
246
+ }
247
+ return commits;
248
+ }
249
+ function parseDecorations(decorations, remotes) {
250
+ const trimmed = decorations.trim();
251
+ if (trimmed === "") return [];
252
+ const refs = [];
253
+ for (const token of trimmed.split(", ")) {
254
+ if (token.startsWith("HEAD -> ")) {
255
+ refs.push({ kind: "branch", name: token.slice(8), head: true });
256
+ } else if (token.startsWith("tag: ")) {
257
+ refs.push({ kind: "tag", name: token.slice(5), head: false });
258
+ } else if (remotes.some((remote) => token === remote || token.startsWith(`${remote}/`))) {
259
+ refs.push({ kind: "remote", name: token, head: false });
260
+ } else {
261
+ refs.push({ kind: "branch", name: token, head: false });
262
+ }
263
+ }
264
+ return refs;
265
+ }
266
+ function parseShowMeta(output) {
267
+ const trimmed = output.trimEnd();
268
+ if (trimmed === "") return null;
269
+ const [hash, shortHash, subject, author, dateIso, ...bodyParts] = trimmed.split(LOG_SEP);
270
+ if (hash === void 0 || hash === "") return null;
271
+ return {
272
+ commit: {
273
+ hash,
274
+ shortHash: shortHash ?? "",
275
+ subject: subject ?? "",
276
+ author: author ?? "",
277
+ dateIso: dateIso ?? ""
278
+ },
279
+ body: bodyParts.join(LOG_SEP).trimEnd()
280
+ };
281
+ }
282
+ function nameStatusCode(code) {
283
+ switch (code) {
284
+ case "A":
285
+ return "added";
286
+ case "D":
287
+ return "deleted";
288
+ case "R":
289
+ return "renamed";
290
+ case "T":
291
+ return "typechange";
292
+ case "U":
293
+ return "conflicted";
294
+ default:
295
+ return "modified";
296
+ }
297
+ }
298
+ function parseNameStatusOutput(output) {
299
+ const raw = output.split(NUL);
300
+ const segments = raw[raw.length - 1] === "" ? raw.slice(0, -1) : raw;
301
+ const rows = [];
302
+ for (let i = 0; i < segments.length; i += 1) {
303
+ const entry = segments[i] ?? "";
304
+ if (entry === "") continue;
305
+ const code = entry[0] ?? " ";
306
+ if (code === "R" || code === "C") {
307
+ rows.push({ path: segments[i + 2] ?? "", status: nameStatusCode(code) });
308
+ i += 2;
309
+ } else {
310
+ rows.push({ path: segments[i + 1] ?? "", status: nameStatusCode(code) });
311
+ i += 1;
312
+ }
313
+ }
314
+ return rows;
315
+ }
217
316
  function parseBranchOutput(output) {
218
317
  const trimmed = output.trim();
219
318
  return trimmed === "" ? null : trimmed;
@@ -296,7 +395,7 @@ async function snapshotForSession(deps, config, sessionId) {
296
395
  if ("failure" in headRun) return { ok: false, error: headRun.failure };
297
396
  if (headRun.run.timedOut) return { ok: false, error: { code: "timeout" } };
298
397
  const head = headRun.run.exitCode === 0 ? headRun.run.stdout.trim() || null : null;
299
- const status = await runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z", "--branch"], root, "status", deps.signal);
398
+ const status = await runCommand(deps.run, ["git", "status", "--porcelain=v1", "-z", "--branch", "--untracked-files=all"], root, "status", deps.signal);
300
399
  if ("failure" in status) return { ok: false, error: status.failure };
301
400
  if (status.run.timedOut) return { ok: false, error: { code: "timeout" } };
302
401
  if (status.run.exitCode !== 0) {
@@ -334,15 +433,15 @@ import { resolve, sep } from "node:path";
334
433
  function buildArgv(action, root) {
335
434
  switch (action.kind) {
336
435
  case "stage":
337
- return withPaths(["git", "add", "--"], action.paths, root);
436
+ return withPaths([["git", "add", "--"]], action.paths, root);
338
437
  case "stage-all":
339
438
  return { argv: [["git", "add", "-A"]] };
340
439
  case "unstage":
341
- return withPaths(["git", "restore", "--staged", "--"], action.paths, root);
440
+ return withPaths([["git", "restore", "--staged", "--"]], action.paths, root);
342
441
  case "unstage-all":
343
442
  return { argv: [["git", "restore", "--staged", "--", "."]] };
344
443
  case "discard":
345
- return withPaths(["git", "restore", "--"], action.paths, root);
444
+ return withPaths([["git", "restore", "--"]], action.paths, root);
346
445
  case "discard-all":
347
446
  return { argv: [["git", "restore", "--staged", "--", "."], ["git", "restore", "--", "."]] };
348
447
  case "commit": {
@@ -350,16 +449,30 @@ function buildArgv(action, root) {
350
449
  if (action.paths === void 0 || action.paths.length === 0) {
351
450
  return { argv: [["git", "commit", "-m", message]] };
352
451
  }
353
- return withPaths(["git", "commit", "-m", message, "--"], action.paths, root);
452
+ return withPaths([["git", "add", "--"], ["git", "commit", "-m", message, "--"]], action.paths, root);
354
453
  }
454
+ case "branch-create": {
455
+ const from = action.from === void 0 || action.from === "" ? [] : [action.from];
456
+ return { argv: [["git", "branch", action.name, ...from]] };
457
+ }
458
+ case "branch-checkout":
459
+ return { argv: [["git", "checkout", action.name]] };
460
+ case "branch-delete":
461
+ return { argv: [["git", "branch", action.force === true ? "-D" : "-d", action.name]] };
462
+ case "fetch":
463
+ return { argv: [["git", "fetch", "--all", "--prune"]] };
355
464
  }
356
465
  }
357
- function withPaths(prefix, paths, root) {
466
+ function isValidBranchName(name) {
467
+ if (name === "" || name.startsWith("-") || name.includes("..") || name.endsWith("/") || name.includes("//")) return false;
468
+ return /^[A-Za-z0-9._/-]+$/.test(name);
469
+ }
470
+ function withPaths(prefixes, paths, root) {
358
471
  if (paths.length === 0) return { error: "no paths given" };
359
472
  for (const path of paths) {
360
473
  if (!isSafePath(path, root)) return { error: `unsafe path: ${path}` };
361
474
  }
362
- return { argv: [[...prefix, ...paths]] };
475
+ return { argv: prefixes.map((prefix) => [...prefix, ...paths]) };
363
476
  }
364
477
  function isSafePath(path, root) {
365
478
  if (path === "") return false;
@@ -374,6 +487,12 @@ function operationError(failure) {
374
487
  }
375
488
  return { ok: false, error: failure };
376
489
  }
490
+ function classifyOperationError(kind, message) {
491
+ if (kind === "branch-checkout" && /would be overwritten by checkout|将被 checkout 覆盖|有未跟踪工作区文件将会被 checkout 覆盖/i.test(message)) {
492
+ return "local-changes-block";
493
+ }
494
+ return "git-error";
495
+ }
377
496
  async function runAction(deps, config, request) {
378
497
  const workspace = await resolveWorkspace(deps, request.sessionId);
379
498
  if (!workspace.ok) return operationError(workspace.error);
@@ -381,6 +500,13 @@ async function runAction(deps, config, request) {
381
500
  if (request.action.kind === "commit" && request.action.message.trim() === "") {
382
501
  return { ok: false, error: { code: "git-error", message: "commit message is empty" } };
383
502
  }
503
+ const kind = request.action.kind;
504
+ if (kind === "branch-create" || kind === "branch-checkout" || kind === "branch-delete") {
505
+ const name = request.action.name;
506
+ if (!isValidBranchName(name)) {
507
+ return { ok: false, error: { code: "invalid-name", message: `invalid branch name: ${name}` } };
508
+ }
509
+ }
384
510
  const built = buildArgv(request.action, root);
385
511
  if ("error" in built) return { ok: false, error: { code: "invalid-path", message: built.error } };
386
512
  let lastStdout = "";
@@ -390,10 +516,11 @@ async function runAction(deps, config, request) {
390
516
  if (outcome.run.timedOut) return { ok: false, error: { code: "timeout" } };
391
517
  if (outcome.run.exitCode !== 0) {
392
518
  const message = outcome.run.stderr.trim() || outcome.run.stdout.trim();
519
+ const code = classifyOperationError(request.action.kind, message);
393
520
  return {
394
521
  ok: false,
395
522
  error: {
396
- code: "git-error",
523
+ code,
397
524
  message: message !== "" ? message : `git ${request.action.kind} exited ${String(outcome.run.exitCode)}`
398
525
  }
399
526
  };
@@ -405,72 +532,311 @@ async function runAction(deps, config, request) {
405
532
  return { ok: true, snapshot: snapshot.value, ...lastStdout === "" ? {} : { output: lastStdout } };
406
533
  }
407
534
 
535
+ // src/host/queries.ts
536
+ var LOG_FORMAT = "%H%x1f%h%x1f%s%x1f%an%x1f%aI";
537
+ var GRAPH_FORMAT = "%H%x1f%h%x1f%s%x1f%an%x1f%aI%x1f%P%x1f%D";
538
+ var MAX_HISTORY_LIMIT = 1e3;
539
+ function isValidRef(ref) {
540
+ return ref !== "" && !/\s/.test(ref);
541
+ }
542
+ async function runQuery(deps, config, request) {
543
+ void config;
544
+ const workspace = await resolveWorkspace(deps, request.sessionId);
545
+ if (!workspace.ok) return { ok: false, error: operationError(workspace.error).error };
546
+ const root = workspace.root;
547
+ const query = request.query;
548
+ switch (query.kind) {
549
+ case "history":
550
+ return historyQuery(deps, root, query);
551
+ case "diff":
552
+ return diffQuery(deps, root, query.path, query.base);
553
+ case "show":
554
+ return showQuery(deps, root, query.ref);
555
+ case "branches":
556
+ return branchesQuery(deps, root);
557
+ case "tags":
558
+ return tagsQuery(deps, root);
559
+ case "authors":
560
+ return authorsQuery(deps, root);
561
+ }
562
+ }
563
+ async function historyQuery(deps, root, query) {
564
+ const safeLimit = Math.min(Math.max(Math.floor(query.limit), 0), MAX_HISTORY_LIMIT);
565
+ const safeSkip = Math.max(Math.floor(query.skip), 0);
566
+ if (query.ref !== void 0 && !isValidRef(query.ref)) {
567
+ return { ok: false, error: { code: "invalid-name", message: `invalid ref: ${query.ref}` } };
568
+ }
569
+ const search = query.search?.trim() ?? "";
570
+ const hexLike = /^[0-9a-f]{7,40}$/i.test(search);
571
+ const scope = hexLike ? [] : query.ref === void 0 ? ["--all"] : [query.ref];
572
+ const noWalk = hexLike ? ["--no-walk", search] : [];
573
+ const filters = [];
574
+ if (search !== "" && !hexLike) filters.push("--regexp-ignore-case", "--extended-regexp", `--grep=${search}`);
575
+ const author = query.author?.trim() ?? "";
576
+ if (author !== "") filters.push(`--author=${author}`);
577
+ const since = query.since?.trim() ?? "";
578
+ if (since !== "") filters.push(`--since=${since}`);
579
+ const log = await runCommand(
580
+ deps.run,
581
+ // -n/--skip 前置:git 的 `-n N` 出现在 `--no-walk` 之后会重置 no-walk
582
+ // (hexLike 会错误列出全部祖先),前置则 `-n 1000 --no-walk x` 恒返回单条。
583
+ ["git", "log", ...filters, `--skip=${String(safeSkip)}`, "-n", String(safeLimit), ...noWalk, ...scope, `--format=${GRAPH_FORMAT}`],
584
+ root,
585
+ "log",
586
+ deps.signal
587
+ );
588
+ if ("failure" in log) return { ok: false, error: operationError(log.failure).error };
589
+ if (log.run.timedOut) return { ok: false, error: { code: "timeout" } };
590
+ if (log.run.exitCode !== 0) {
591
+ if (log.run.stderr.includes("does not have any commits")) {
592
+ return { ok: true, value: { kind: "history", commits: [], total: 0 } };
593
+ }
594
+ if (hexLike && /unknown revision|bad revision|ambiguous/i.test(log.run.stderr)) {
595
+ return { ok: true, value: { kind: "history", commits: [], total: 0 } };
596
+ }
597
+ return gitError("log", log.run.stderr, log.run.stdout);
598
+ }
599
+ let total = 0;
600
+ const count = await runCommand(deps.run, ["git", "rev-list", "--count", ...noWalk, ...scope, ...filters], root, "rev-list", deps.signal);
601
+ if ("run" in count && count.run.exitCode === 0) {
602
+ const parsed = Number(count.run.stdout.trim());
603
+ if (Number.isFinite(parsed) && parsed >= 0) total = parsed;
604
+ }
605
+ let remotes = [];
606
+ const remoteRun = await runCommand(deps.run, ["git", "remote"], root, "remote", deps.signal);
607
+ if ("run" in remoteRun && remoteRun.run.exitCode === 0) {
608
+ remotes = remoteRun.run.stdout.split("\n").map((s) => s.trim()).filter((s) => s !== "");
609
+ }
610
+ return { ok: true, value: { kind: "history", commits: parseGraphLogOutput(log.run.stdout, remotes), total } };
611
+ }
612
+ async function diffQuery(deps, root, path, base) {
613
+ if (!isSafePath(path, root)) return { ok: false, error: { code: "invalid-path", message: `unsafe path: ${path}` } };
614
+ const argv = base === "staged" ? ["git", "diff", "--cached", "-U999999", "--", path] : ["git", "diff", "-U999999", "--", path];
615
+ const run = await runCommand(deps.run, argv, root, "diff", deps.signal);
616
+ if ("failure" in run) return { ok: false, error: operationError(run.failure).error };
617
+ if (run.run.timedOut) return { ok: false, error: { code: "timeout" } };
618
+ if (run.run.exitCode !== 0) return gitError("diff", run.run.stderr, run.run.stdout);
619
+ if (run.run.stdout !== "" || base === "staged") {
620
+ return { ok: true, value: { kind: "diff", path, text: run.run.stdout } };
621
+ }
622
+ const ni = await runCommand(deps.run, ["git", "diff", "--no-index", "-U999999", "--", "/dev/null", path], root, "diff --no-index", deps.signal);
623
+ if ("failure" in ni) return { ok: false, error: operationError(ni.failure).error };
624
+ if (ni.run.timedOut) return { ok: false, error: { code: "timeout" } };
625
+ if (ni.run.exitCode !== 0 && ni.run.exitCode !== 1) return gitError("diff", ni.run.stderr, ni.run.stdout);
626
+ return { ok: true, value: { kind: "diff", path, text: ni.run.stdout } };
627
+ }
628
+ async function showQuery(deps, root, ref) {
629
+ if (!isValidRef(ref)) return { ok: false, error: { code: "invalid-name", message: `invalid ref: ${ref}` } };
630
+ const meta = await runCommand(
631
+ deps.run,
632
+ ["git", "show", "-s", `--format=${LOG_FORMAT}%x1f%b`, ref],
633
+ root,
634
+ "show",
635
+ deps.signal
636
+ );
637
+ if ("failure" in meta) return { ok: false, error: operationError(meta.failure).error };
638
+ if (meta.run.timedOut) return { ok: false, error: { code: "timeout" } };
639
+ if (meta.run.exitCode !== 0) return gitError("show", meta.run.stderr, meta.run.stdout);
640
+ const stat2 = await runCommand(
641
+ deps.run,
642
+ ["git", "-c", "core.quotePath=false", "show", "--format=", "--name-status", "-z", ref],
643
+ root,
644
+ "show --name-status",
645
+ deps.signal
646
+ );
647
+ if ("failure" in stat2) return { ok: false, error: operationError(stat2.failure).error };
648
+ if (stat2.run.timedOut) return { ok: false, error: { code: "timeout" } };
649
+ if (stat2.run.exitCode !== 0) return gitError("show", stat2.run.stderr, stat2.run.stdout);
650
+ const parsed = parseShowMeta(meta.run.stdout);
651
+ return {
652
+ ok: true,
653
+ value: {
654
+ kind: "show",
655
+ ref,
656
+ commit: parsed?.commit ?? null,
657
+ body: parsed?.body ?? "",
658
+ stats: parseNameStatusOutput(stat2.run.stdout)
659
+ }
660
+ };
661
+ }
662
+ async function authorsQuery(deps, root) {
663
+ const run = await runCommand(deps.run, ["git", "log", "--all", "-n", "1000", "--format=%an"], root, "log authors", deps.signal);
664
+ if ("failure" in run) return { ok: false, error: operationError(run.failure).error };
665
+ if (run.run.timedOut) return { ok: false, error: { code: "timeout" } };
666
+ if (run.run.exitCode !== 0) return { ok: true, value: { kind: "authors", authors: [] } };
667
+ const authors = [...new Set(run.run.stdout.split("\n").map((s) => s.trim()).filter((s) => s !== ""))].sort().slice(0, 100);
668
+ return { ok: true, value: { kind: "authors", authors } };
669
+ }
670
+ async function tagsQuery(deps, root) {
671
+ const FORMAT = "--format=%(refname:short)%09%(objectname:short)";
672
+ const run = await runCommand(deps.run, ["git", "tag", FORMAT], root, "tag", deps.signal);
673
+ if ("failure" in run) return { ok: false, error: operationError(run.failure).error };
674
+ if (run.run.timedOut) return { ok: false, error: { code: "timeout" } };
675
+ if (run.run.exitCode !== 0) return gitError("tag", run.run.stderr, run.run.stdout);
676
+ return { ok: true, value: { kind: "tags", tags: parseBranchList(run.run.stdout) } };
677
+ }
678
+ async function branchesQuery(deps, root) {
679
+ const LOCAL_FORMAT = "--format=%(refname:short)%09%(objectname:short)%09%(upstream:short)%09%(upstream:track)";
680
+ const REMOTE_FORMAT = "--format=%(refname:short)%09%(objectname:short)";
681
+ const local = await runCommand(deps.run, ["git", "branch", LOCAL_FORMAT], root, "branch", deps.signal);
682
+ if ("failure" in local) return { ok: false, error: operationError(local.failure).error };
683
+ if (local.run.timedOut) return { ok: false, error: { code: "timeout" } };
684
+ if (local.run.exitCode !== 0) return gitError("branch", local.run.stderr, local.run.stdout);
685
+ const remote = await runCommand(deps.run, ["git", "branch", "-r", REMOTE_FORMAT], root, "branch -r", deps.signal);
686
+ if ("failure" in remote) return { ok: false, error: operationError(remote.failure).error };
687
+ if (remote.run.timedOut) return { ok: false, error: { code: "timeout" } };
688
+ if (remote.run.exitCode !== 0) return gitError("branch -r", remote.run.stderr, remote.run.stdout);
689
+ const current = await runCommand(deps.run, ["git", "branch", "--show-current"], root, "branch", deps.signal);
690
+ const currentName = "run" in current && current.run.exitCode === 0 ? parseBranchOutput(current.run.stdout) : null;
691
+ let defaultBranch = null;
692
+ const def = await runCommand(deps.run, ["git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"], root, "symbolic-ref", deps.signal);
693
+ if ("run" in def && def.run.exitCode === 0) {
694
+ const value = def.run.stdout.trim();
695
+ const slash = value.indexOf("/");
696
+ defaultBranch = value === "" ? null : slash === -1 ? value : value.slice(slash + 1);
697
+ }
698
+ return {
699
+ ok: true,
700
+ value: {
701
+ kind: "branches",
702
+ current: currentName,
703
+ defaultBranch,
704
+ local: parseBranchList(local.run.stdout),
705
+ remote: parseBranchList(remote.run.stdout).filter((branch) => !branch.name.endsWith("/HEAD"))
706
+ }
707
+ };
708
+ }
709
+ function parseBranchList(output) {
710
+ const branches = [];
711
+ for (const line of output.split("\n")) {
712
+ if (line === "") continue;
713
+ const parts = line.split(" ");
714
+ const name = parts[0];
715
+ const hash = parts[1];
716
+ if (name === void 0 || name === "") continue;
717
+ const track = parts[3] ?? "";
718
+ const aheadMatch = /ahead (\d+)/.exec(track);
719
+ const behindMatch = /behind (\d+)/.exec(track);
720
+ const ahead = aheadMatch ? Number(aheadMatch[1]) : 0;
721
+ const behind = behindMatch ? Number(behindMatch[1]) : 0;
722
+ branches.push({
723
+ name,
724
+ shortHash: hash === void 0 || hash === "" ? null : hash,
725
+ ...ahead > 0 ? { ahead } : {},
726
+ ...behind > 0 ? { behind } : {}
727
+ });
728
+ }
729
+ return branches;
730
+ }
731
+ function gitError(label, stderr, stdout) {
732
+ const message = stderr.trim() || stdout.trim();
733
+ return {
734
+ ok: false,
735
+ error: {
736
+ code: "git-error",
737
+ message: message !== "" ? message : `git ${label} failed`
738
+ }
739
+ };
740
+ }
741
+
742
+ // src/contracts/host-endpoints.ts
743
+ function createHostEndpoints(deps, config) {
744
+ return {
745
+ snapshot(request, signal) {
746
+ const merged = mergeSignals(deps.signal, signal);
747
+ const effectiveDeps = merged === deps.signal ? deps : { ...deps, signal: merged };
748
+ return snapshotForSession(effectiveDeps, config, request.sessionId);
749
+ },
750
+ run(request, signal) {
751
+ const merged = mergeSignals(deps.signal, signal);
752
+ const effectiveDeps = merged === deps.signal ? deps : { ...deps, signal: merged };
753
+ return runAction(effectiveDeps, config, request);
754
+ },
755
+ query(request, signal) {
756
+ const merged = mergeSignals(deps.signal, signal);
757
+ const effectiveDeps = merged === deps.signal ? deps : { ...deps, signal: merged };
758
+ return runQuery(effectiveDeps, config, request);
759
+ }
760
+ };
761
+ }
762
+ function mergeSignals(a, b) {
763
+ if (a === void 0) return b;
764
+ if (b === void 0) return a;
765
+ return AbortSignal.any([a, b]);
766
+ }
767
+
408
768
  // src/host/index.ts
409
- var _run_dec, _snapshot_dec, _a, _init;
410
- var GitStatusService = class extends (_a = TypertRemoteService, _snapshot_dec = [Remote("snapshot")], _run_dec = [Remote("run")], _a) {
769
+ var _query_dec, _run_dec, _snapshot_dec, _a, _init;
770
+ var GitStatusService = class extends (_a = TypertRemoteService, _snapshot_dec = [Remote("snapshot")], _run_dec = [Remote("run")], _query_dec = [Remote("query")], _a) {
411
771
  constructor(ctx, config) {
412
772
  super(ctx, "gitInfo");
413
773
  __runInitializers(_init, 5, this);
414
- __publicField(this, "config");
415
- this.config = normalizeConfig(config);
774
+ __publicField(this, "endpoints");
775
+ const normalizedConfig = normalizeConfig(config);
776
+ const deps = this.buildDeps(ctx, normalizedConfig);
777
+ this.endpoints = createHostEndpoints(deps, normalizedConfig);
416
778
  }
417
- /** Adapter face shared by both endpoints (injected services + runner). */
418
- deps(signal) {
419
- const subprocess = this.ctx.get("subprocess");
779
+ /** Cordis 服务适配为结构化 SnapshotDeps。 */
780
+ buildDeps(ctx, config) {
781
+ const subprocess = ctx.get("subprocess");
420
782
  if (subprocess === void 0) {
421
- return { failure: { code: "git-unavailable", detail: "subprocess service unavailable" } };
783
+ return {
784
+ run: { run: async () => {
785
+ throw new Error("subprocess service unavailable");
786
+ } },
787
+ fs: { realpath, stat },
788
+ sessions: { liveCwd: () => void 0, persistedMeta: async () => void 0 }
789
+ };
422
790
  }
423
- const sessions = this.ctx.get("sessions");
424
- const persistence = this.ctx.get("sessionPersistence");
425
- const runner = createGitRunner(subprocess, this.config.timeoutMs, this.config.maxStatusBytes);
791
+ const sessions = ctx.get("sessions");
792
+ const persistence = ctx.get("sessionPersistence");
426
793
  return {
427
- deps: {
428
- run: runner,
429
- fs: { realpath, stat },
430
- sessions: {
431
- liveCwd: (id) => sessions?.get(id)?.header?.cwd,
432
- persistedMeta: async (id) => {
433
- if (persistence === void 0) return void 0;
434
- try {
435
- const inspection = await persistence.inspect(id);
436
- return { cwd: inspection.meta.cwd };
437
- } catch {
438
- return void 0;
439
- }
794
+ run: createGitRunner(subprocess, config.timeoutMs, config.maxStatusBytes),
795
+ fs: { realpath, stat },
796
+ sessions: {
797
+ liveCwd: (id) => sessions?.get(id)?.header?.cwd,
798
+ persistedMeta: async (id) => {
799
+ if (persistence === void 0) return void 0;
800
+ try {
801
+ const inspection = await persistence.inspect(id);
802
+ return { cwd: inspection.meta.cwd };
803
+ } catch {
804
+ return void 0;
440
805
  }
441
- },
442
- signal
806
+ }
443
807
  }
444
808
  };
445
809
  }
446
810
  async snapshot(request, signal) {
447
- const adapted = this.deps(signal);
448
- if ("failure" in adapted) return { ok: false, error: adapted.failure };
449
- return snapshotForSession(adapted.deps, this.config, request.sessionId);
811
+ return this.endpoints.snapshot(request, signal);
450
812
  }
451
813
  async run(request, signal) {
452
- const adapted = this.deps(signal);
453
- if ("failure" in adapted) {
454
- return { ok: false, error: { code: "git-error", message: adapted.failure.detail } };
455
- }
456
- return runAction(adapted.deps, this.config, request);
814
+ return this.endpoints.run(request, signal);
815
+ }
816
+ async query(request, signal) {
817
+ return this.endpoints.query(request, signal);
457
818
  }
458
819
  };
459
820
  _init = __decoratorStart(_a);
460
821
  __decorateElement(_init, 1, "snapshot", _snapshot_dec, GitStatusService);
461
822
  __decorateElement(_init, 1, "run", _run_dec, GitStatusService);
823
+ __decorateElement(_init, 1, "query", _query_dec, GitStatusService);
462
824
  __decoratorMetadata(_init, GitStatusService);
463
825
  __publicField(GitStatusService, "inject", ["subprocess", "sessions", "sessionPersistence"]);
464
826
  var index_default = GitStatusService;
465
827
  export {
466
828
  DEFAULT_CONFIG,
467
829
  GitStatusService,
830
+ createHostEndpoints,
468
831
  index_default as default,
469
832
  isSafePath,
833
+ isValidBranchName,
470
834
  normalizeConfig,
471
835
  parseBranchOutput,
472
836
  parseLogOutput,
837
+ parseNameStatusOutput,
473
838
  parseStatusOutput,
474
- runAction
839
+ runAction,
840
+ runQuery
475
841
  };
476
842
  //# sourceMappingURL=index.js.map