dsh-ccgui 0.0.0-stage → 1.0.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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +327 -2
  3. package/THIRD_PARTY_NOTICES.md +64 -0
  4. package/assets/logo.png +0 -0
  5. package/cordis.patch.yml +32 -0
  6. package/lib/abortable.d.ts +12 -0
  7. package/lib/abortable.js +27 -0
  8. package/lib/adapter.d.ts +39 -0
  9. package/lib/adapter.js +64 -0
  10. package/lib/async-queue.d.ts +26 -0
  11. package/lib/async-queue.js +55 -0
  12. package/lib/cli-session.d.ts +99 -0
  13. package/lib/cli-session.js +384 -0
  14. package/lib/cli-tool.d.ts +18 -0
  15. package/lib/cli-tool.js +101 -0
  16. package/lib/client.js +4039 -0
  17. package/lib/constants.d.ts +22 -0
  18. package/lib/constants.js +22 -0
  19. package/lib/git/git-read.d.ts +65 -0
  20. package/lib/git/git-read.js +139 -0
  21. package/lib/git/git-requests.d.ts +44 -0
  22. package/lib/git/git-requests.js +153 -0
  23. package/lib/git/git-routes.d.ts +38 -0
  24. package/lib/git/git-routes.js +78 -0
  25. package/lib/git/git-run.d.ts +58 -0
  26. package/lib/git/git-run.js +97 -0
  27. package/lib/git/git-status.d.ts +51 -0
  28. package/lib/git/git-status.js +122 -0
  29. package/lib/git/git-write.d.ts +75 -0
  30. package/lib/git/git-write.js +134 -0
  31. package/lib/index.d.ts +78 -0
  32. package/lib/index.js +229 -0
  33. package/lib/model-cache.d.ts +33 -0
  34. package/lib/model-cache.js +101 -0
  35. package/lib/model-catalog.d.ts +63 -0
  36. package/lib/model-catalog.js +107 -0
  37. package/lib/model-options.d.ts +26 -0
  38. package/lib/model-options.js +46 -0
  39. package/lib/model-probe.d.ts +16 -0
  40. package/lib/model-probe.js +91 -0
  41. package/lib/npx-cache.d.ts +39 -0
  42. package/lib/npx-cache.js +97 -0
  43. package/lib/permission.d.ts +17 -0
  44. package/lib/permission.js +34 -0
  45. package/lib/prompt-images.d.ts +44 -0
  46. package/lib/prompt-images.js +46 -0
  47. package/lib/request-input.d.ts +42 -0
  48. package/lib/request-input.js +113 -0
  49. package/lib/session/connect.d.ts +41 -0
  50. package/lib/session/connect.js +68 -0
  51. package/lib/session/drive-tool.d.ts +39 -0
  52. package/lib/session/drive-tool.js +37 -0
  53. package/lib/session/model-state.d.ts +40 -0
  54. package/lib/session/model-state.js +68 -0
  55. package/lib/session/outcomes.d.ts +31 -0
  56. package/lib/session/outcomes.js +67 -0
  57. package/lib/session/permission-queue.d.ts +24 -0
  58. package/lib/session/permission-queue.js +33 -0
  59. package/lib/session/read-step.d.ts +31 -0
  60. package/lib/session/read-step.js +53 -0
  61. package/lib/session/tool-mirror.d.ts +65 -0
  62. package/lib/session/tool-mirror.js +131 -0
  63. package/lib/session/turn-queue.d.ts +76 -0
  64. package/lib/session/turn-queue.js +102 -0
  65. package/lib/session/types.d.ts +58 -0
  66. package/lib/session/types.js +6 -0
  67. package/lib/session-registry.d.ts +124 -0
  68. package/lib/session-registry.js +189 -0
  69. package/lib/stdio-transport.d.ts +28 -0
  70. package/lib/stdio-transport.js +164 -0
  71. package/lib/stream-blocks.d.ts +52 -0
  72. package/lib/stream-blocks.js +95 -0
  73. package/lib/tool-record.d.ts +100 -0
  74. package/lib/tool-record.js +167 -0
  75. package/lib/transport.d.ts +35 -0
  76. package/lib/transport.js +5 -0
  77. package/package.json +136 -4
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Run one git command in a working directory through the harness's subprocess service, so
3
+ * git resolves in the same execution world as the CLIs (a remote or sandboxed host included).
4
+ * @module dsh-ccgui/git/git-run
5
+ */
6
+ /** Plenty for status and numstat of a large checkout; diffs pass their own cap. */
7
+ const DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
8
+ /** stderr only carries messages for the user. */
9
+ const STDERR_MAX_BYTES = 64 * 1024;
10
+ const GRACE_MS = 2_000;
11
+ /**
12
+ * Paths stay raw UTF-8 (no octal quoting), and nothing waits on a terminal: a push that
13
+ * needs a password fails fast instead of hanging on a prompt nobody can see.
14
+ *
15
+ * The panel reads status on a timer, without asking anyone, in a repository whose config an
16
+ * agent may have edited; `core.fsmonitor` names a command git would run on every status, so
17
+ * it is switched off here. Diff drivers are switched off where the diffs are read.
18
+ *
19
+ * Paths arrive from the panel as file names, so pathspec patterns are off: discarding
20
+ * `src/[id].tsx` must not also discard `src/d.tsx`, and `:memo.txt` is a file, not magic.
21
+ */
22
+ const GIT_PREFIX = ['git', '-c', 'core.quotePath=false', '-c', 'color.ui=false', '-c', 'core.fsmonitor=false'];
23
+ const GIT_ENV = { GIT_TERMINAL_PROMPT: '0', GIT_OPTIONAL_LOCKS: '0', GIT_LITERAL_PATHSPECS: '1', LC_ALL: 'C' };
24
+ /**
25
+ * Run `git <args>` in `cwd` and collect its output. A non-zero exit is reported, not thrown;
26
+ * a failure to start git at all rejects.
27
+ * @param spawn - the subprocess service's spawn.
28
+ * @param cwd - the working directory.
29
+ * @param args - git's arguments, never shell-interpreted.
30
+ * @param options - abort signal and output cap.
31
+ * @returns the exit code and the collected output.
32
+ */
33
+ export async function runGit(spawn, cwd, args, options = {}) {
34
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
35
+ const child = spawn({
36
+ argv: [...GIT_PREFIX, ...args],
37
+ cwd,
38
+ env: GIT_ENV,
39
+ graceMs: GRACE_MS,
40
+ signal: options.signal,
41
+ stdio: {
42
+ stdin: options.input === undefined ? 'ignore' : { data: options.input },
43
+ stdout: { maxBytes },
44
+ stderr: { maxBytes: STDERR_MAX_BYTES },
45
+ },
46
+ });
47
+ const outcome = await child.done;
48
+ const stdout = child.collected.stdout?.readFrom(0);
49
+ const stderr = child.collected.stderr?.readFrom(0);
50
+ return {
51
+ exitCode: outcome.exitCode,
52
+ stdout: stdout?.text ?? '',
53
+ stderr: stderr?.text ?? '',
54
+ truncated: stdout?.lossy ?? false,
55
+ };
56
+ }
57
+ /** A git command that failed; the message is ready to show the user. */
58
+ export class GitError extends Error {
59
+ name = 'GitError';
60
+ }
61
+ /** stderr lines git adds around the message that matters, such as push hints. */
62
+ const NOISE = /^(hint:|remote:\s*$)/;
63
+ function reasonOf(result) {
64
+ // Some refusals, such as "nothing to commit", are printed on stdout.
65
+ const text = result.stderr.trim() === '' ? result.stdout : result.stderr;
66
+ const lines = text
67
+ .split('\n')
68
+ .map((line) => line.trim())
69
+ .filter((line) => line !== '' && !NOISE.test(line));
70
+ return lines.join('\n') || `git 退出码 ${String(result.exitCode)}`;
71
+ }
72
+ /**
73
+ * Describe a failed command for the user.
74
+ * @param result - what git reported.
75
+ * @param failure - what failed, in front of git's own reason.
76
+ * @returns the error to throw.
77
+ */
78
+ export function failureOf(result, failure) {
79
+ return new GitError(`${failure}:${reasonOf(result)}`);
80
+ }
81
+ /**
82
+ * Run git and require success.
83
+ * @param spawn - the subprocess service's spawn.
84
+ * @param cwd - the working directory.
85
+ * @param args - git's arguments.
86
+ * @param how - what the user is told failed, in front of git's own reason; with run options
87
+ * (abort signal, output cap, stdin) as an object.
88
+ * @returns the result of a command that exited with 0.
89
+ * @throws GitError when git exits with anything else.
90
+ */
91
+ export async function mustGit(spawn, cwd, args, how) {
92
+ const { failure, ...options } = typeof how === 'string' ? { failure: how } : how;
93
+ const result = await runGit(spawn, cwd, args, options);
94
+ if (result.exitCode !== 0)
95
+ throw failureOf(result, failure);
96
+ return result;
97
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Parse `git status --porcelain=v2 --branch -z` and `git diff --numstat -z` into the shape
3
+ * the 变更 panel draws: one list per group (已暂存, 未暂存, 未跟踪), each entry with a status
4
+ * letter and line counts, plus the branch and how far it is from its upstream.
5
+ * @module dsh-ccgui/git/git-status
6
+ */
7
+ /** M modified, A added, D deleted, R renamed, C copied, T type changed, U conflicted, ? untracked. */
8
+ type ChangeStatus = 'M' | 'A' | 'D' | 'R' | 'C' | 'T' | 'U' | '?';
9
+ export interface ChangeEntry {
10
+ readonly path: string;
11
+ /** The path before a rename or copy. */
12
+ readonly origPath?: string;
13
+ readonly status: ChangeStatus;
14
+ readonly additions?: number;
15
+ readonly deletions?: number;
16
+ }
17
+ export interface GitStatus {
18
+ /** Branch name, or undefined on a detached HEAD. */
19
+ readonly branch: string | undefined;
20
+ /** Set only when the branch has an upstream. */
21
+ readonly ahead: number | undefined;
22
+ readonly behind: number | undefined;
23
+ readonly staged: readonly ChangeEntry[];
24
+ readonly unstaged: readonly ChangeEntry[];
25
+ readonly untracked: readonly ChangeEntry[];
26
+ }
27
+ /** Line counts per path; binary files have none. */
28
+ export type LineCounts = ReadonlyMap<string, {
29
+ readonly additions: number;
30
+ readonly deletions: number;
31
+ }>;
32
+ /**
33
+ * Parse porcelain v2 status output.
34
+ * @param output - stdout of `git status --porcelain=v2 --branch -z --untracked-files=all`.
35
+ * @returns the branch and the three groups, without line counts.
36
+ */
37
+ export declare function parseStatus(output: string): GitStatus;
38
+ /**
39
+ * Parse numstat output. A rename's counts are filed under its new path.
40
+ * @param output - stdout of `git diff --numstat -z`.
41
+ * @returns line counts per path; binary files are left out.
42
+ */
43
+ export declare function parseNumstat(output: string): LineCounts;
44
+ /**
45
+ * Attach line counts to entries that have them.
46
+ * @param entries - one group's entries.
47
+ * @param counts - that group's numstat.
48
+ * @returns new entries; ones without counts are returned as they were.
49
+ */
50
+ export declare function withCounts(entries: readonly ChangeEntry[], counts: LineCounts): readonly ChangeEntry[];
51
+ export {};
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Parse `git status --porcelain=v2 --branch -z` and `git diff --numstat -z` into the shape
3
+ * the 变更 panel draws: one list per group (已暂存, 未暂存, 未跟踪), each entry with a status
4
+ * letter and line counts, plus the branch and how far it is from its upstream.
5
+ * @module dsh-ccgui/git/git-status
6
+ */
7
+ const STATUS_LETTERS = new Set(['M', 'A', 'D', 'R', 'C', 'T']);
8
+ /** Fields before the path in an ordinary (`1`) and a renamed (`2`) entry, and a conflict (`u`). */
9
+ const ORDINARY_FIELDS = 8;
10
+ const RENAMED_FIELDS = 9;
11
+ const UNMERGED_FIELDS = 10;
12
+ function letterOf(code) {
13
+ return code !== undefined && STATUS_LETTERS.has(code) ? code : undefined;
14
+ }
15
+ /** Split `count` space-separated fields off the front, keeping spaces in the path that follows. */
16
+ function fieldsAndPath(record, count) {
17
+ const fields = record.split(' ', count);
18
+ const head = fields.join(' ').length + 1;
19
+ return { fields, path: record.slice(head) };
20
+ }
21
+ function parseBranchHeader(line, branch) {
22
+ if (line.startsWith('# branch.head ')) {
23
+ const name = line.slice('# branch.head '.length);
24
+ return { ...branch, branch: name === '(detached)' ? undefined : name };
25
+ }
26
+ const ab = /^# branch\.ab \+(\d+) -(\d+)$/.exec(line);
27
+ if (ab === null)
28
+ return branch;
29
+ return { ...branch, ahead: Number(ab[1]), behind: Number(ab[2]) };
30
+ }
31
+ function addTracked(groups, xy, path, origPath) {
32
+ const base = origPath === undefined ? { path } : { path, origPath };
33
+ const index = letterOf(xy[0]);
34
+ const tree = letterOf(xy[1]);
35
+ return {
36
+ ...groups,
37
+ staged: index === undefined ? groups.staged : [...groups.staged, { ...base, status: index }],
38
+ unstaged: tree === undefined ? groups.unstaged : [...groups.unstaged, { ...base, status: tree }],
39
+ };
40
+ }
41
+ function addRecord(groups, records, at) {
42
+ const record = records[at] ?? '';
43
+ const kind = record[0];
44
+ if (kind === '?') {
45
+ return {
46
+ groups: { ...groups, untracked: [...groups.untracked, { path: record.slice(2), status: '?' }] },
47
+ next: at + 1,
48
+ };
49
+ }
50
+ if (kind === '1') {
51
+ const { fields, path } = fieldsAndPath(record, ORDINARY_FIELDS);
52
+ return { groups: addTracked(groups, fields[1] ?? '', path), next: at + 1 };
53
+ }
54
+ if (kind === '2') {
55
+ // The path before the rename is the next NUL-separated record.
56
+ const { fields, path } = fieldsAndPath(record, RENAMED_FIELDS);
57
+ return { groups: addTracked(groups, fields[1] ?? '', path, records[at + 1]), next: at + 2 };
58
+ }
59
+ if (kind === 'u') {
60
+ const { path } = fieldsAndPath(record, UNMERGED_FIELDS);
61
+ return { groups: { ...groups, unstaged: [...groups.unstaged, { path, status: 'U' }] }, next: at + 1 };
62
+ }
63
+ return { groups, next: at + 1 };
64
+ }
65
+ /**
66
+ * Parse porcelain v2 status output.
67
+ * @param output - stdout of `git status --porcelain=v2 --branch -z --untracked-files=all`.
68
+ * @returns the branch and the three groups, without line counts.
69
+ */
70
+ export function parseStatus(output) {
71
+ const records = output.split('\0').filter((record) => record !== '');
72
+ let branch = { branch: undefined, ahead: undefined, behind: undefined };
73
+ let groups = { staged: [], unstaged: [], untracked: [] };
74
+ let at = 0;
75
+ while (at < records.length) {
76
+ const record = records[at] ?? '';
77
+ if (record.startsWith('# ')) {
78
+ branch = parseBranchHeader(record, branch);
79
+ at += 1;
80
+ continue;
81
+ }
82
+ const step = addRecord(groups, records, at);
83
+ groups = step.groups;
84
+ at = step.next;
85
+ }
86
+ return { ...branch, ...groups };
87
+ }
88
+ /**
89
+ * Parse numstat output. A rename's counts are filed under its new path.
90
+ * @param output - stdout of `git diff --numstat -z`.
91
+ * @returns line counts per path; binary files are left out.
92
+ */
93
+ export function parseNumstat(output) {
94
+ const records = output.split('\0');
95
+ const counts = new Map();
96
+ let at = 0;
97
+ while (at < records.length) {
98
+ const [added, deleted, path] = (records[at] ?? '').split('\t');
99
+ // A rename has an empty path here, then the old and the new path as records of their own.
100
+ const target = path === '' ? records[at + 2] : path;
101
+ at += path === '' ? 3 : 1;
102
+ if (target === undefined || added === undefined || deleted === undefined)
103
+ continue;
104
+ const additions = Number(added);
105
+ const deletions = Number(deleted);
106
+ if (Number.isInteger(additions) && Number.isInteger(deletions))
107
+ counts.set(target, { additions, deletions });
108
+ }
109
+ return counts;
110
+ }
111
+ /**
112
+ * Attach line counts to entries that have them.
113
+ * @param entries - one group's entries.
114
+ * @param counts - that group's numstat.
115
+ * @returns new entries; ones without counts are returned as they were.
116
+ */
117
+ export function withCounts(entries, counts) {
118
+ return entries.map((entry) => {
119
+ const count = counts.get(entry.path);
120
+ return count === undefined ? entry : { ...entry, ...count };
121
+ });
122
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The 变更 panel's writes: stage, unstage, discard, commit the selected files, push, pull,
3
+ * and switch or create branches. Paths are relative to the repository's top level and always
4
+ * follow `--`, so a file named like an option stays a file.
5
+ * @module dsh-ccgui/git/git-write
6
+ */
7
+ import type { Spawn } from './git-run.ts';
8
+ /**
9
+ * Stage files, deletions included.
10
+ * @param spawn - the subprocess service's spawn.
11
+ * @param root - the repository's top level.
12
+ * @param paths - files to stage.
13
+ */
14
+ export declare function stage(spawn: Spawn, root: string, paths: readonly string[]): Promise<void>;
15
+ /**
16
+ * Take files out of the index, keeping their changes in the working tree.
17
+ * @param spawn - the subprocess service's spawn.
18
+ * @param root - the repository's top level.
19
+ * @param paths - files to unstage.
20
+ */
21
+ export declare function unstage(spawn: Spawn, root: string, paths: readonly string[]): Promise<void>;
22
+ /**
23
+ * Throw away working-tree changes: tracked files go back to their staged version, untracked
24
+ * files are deleted.
25
+ * @param spawn - the subprocess service's spawn.
26
+ * @param root - the repository's top level.
27
+ * @param target - tracked and untracked files to discard.
28
+ */
29
+ export declare function discard(spawn: Spawn, root: string, target: {
30
+ readonly tracked: readonly string[];
31
+ readonly untracked: readonly string[];
32
+ }): Promise<void>;
33
+ /**
34
+ * Commit exactly the selected files, as they are in the working tree. Files that were staged
35
+ * but not selected stay staged for a later commit, which spares the user the unstage-and-
36
+ * confirm step a commit of the whole index would need.
37
+ * @param spawn - the subprocess service's spawn.
38
+ * @param root - the repository's top level.
39
+ * @param change - the message and the files to commit.
40
+ */
41
+ export declare function commit(spawn: Spawn, root: string, change: {
42
+ readonly message: string;
43
+ readonly paths: readonly string[];
44
+ }): Promise<void>;
45
+ /**
46
+ * Push the current branch. A branch without an upstream is pushed to origin and tracks it.
47
+ * @param spawn - the subprocess service's spawn.
48
+ * @param root - the repository's top level.
49
+ */
50
+ export declare function push(spawn: Spawn, root: string): Promise<void>;
51
+ /**
52
+ * Pull the current branch, fast-forward only: a merge or a rebase needs decisions the panel
53
+ * cannot ask for, so those are left to the terminal.
54
+ * @param spawn - the subprocess service's spawn.
55
+ * @param root - the repository's top level.
56
+ */
57
+ export declare function pull(spawn: Spawn, root: string): Promise<void>;
58
+ /**
59
+ * Switch to a branch. A remote branch is checked out as a local branch that tracks it,
60
+ * unless a local branch of that name exists already.
61
+ * @param spawn - the subprocess service's spawn.
62
+ * @param root - the repository's top level.
63
+ * @param branch - the branch, and whether it names a remote branch such as `origin/main`.
64
+ */
65
+ export declare function checkout(spawn: Spawn, root: string, branch: {
66
+ readonly name: string;
67
+ readonly remote: boolean;
68
+ }): Promise<void>;
69
+ /**
70
+ * Create a branch from HEAD and switch to it.
71
+ * @param spawn - the subprocess service's spawn.
72
+ * @param root - the repository's top level.
73
+ * @param name - the new branch's name.
74
+ */
75
+ export declare function createBranch(spawn: Spawn, root: string, name: string): Promise<void>;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The 变更 panel's writes: stage, unstage, discard, commit the selected files, push, pull,
3
+ * and switch or create branches. Paths are relative to the repository's top level and always
4
+ * follow `--`, so a file named like an option stays a file.
5
+ * @module dsh-ccgui/git/git-write
6
+ */
7
+ import { GitError, mustGit, runGit } from "./git-run.js";
8
+ /** Network commands wait on a remote; everything else is local and quick. */
9
+ const NETWORK_TIMEOUT_MS = 120_000;
10
+ const NO_ORIGIN = '没有名为 origin 的远程仓库';
11
+ async function hasHead(spawn, root) {
12
+ const result = await runGit(spawn, root, ['rev-parse', '--verify', '--quiet', 'HEAD']);
13
+ return result.exitCode === 0;
14
+ }
15
+ /**
16
+ * Stage files, deletions included.
17
+ * @param spawn - the subprocess service's spawn.
18
+ * @param root - the repository's top level.
19
+ * @param paths - files to stage.
20
+ */
21
+ export async function stage(spawn, root, paths) {
22
+ if (paths.length === 0)
23
+ return;
24
+ await mustGit(spawn, root, ['add', '-A', '--', ...paths], '暂存失败');
25
+ }
26
+ /**
27
+ * Take files out of the index, keeping their changes in the working tree.
28
+ * @param spawn - the subprocess service's spawn.
29
+ * @param root - the repository's top level.
30
+ * @param paths - files to unstage.
31
+ */
32
+ export async function unstage(spawn, root, paths) {
33
+ if (paths.length === 0)
34
+ return;
35
+ // Before the first commit there is no HEAD to reset to, so unstaging means removing.
36
+ const args = (await hasHead(spawn, root))
37
+ ? ['reset', '-q', 'HEAD', '--', ...paths]
38
+ : ['rm', '--cached', '-r', '-q', '--', ...paths];
39
+ await mustGit(spawn, root, args, '取消暂存失败');
40
+ }
41
+ /**
42
+ * Throw away working-tree changes: tracked files go back to their staged version, untracked
43
+ * files are deleted.
44
+ * @param spawn - the subprocess service's spawn.
45
+ * @param root - the repository's top level.
46
+ * @param target - tracked and untracked files to discard.
47
+ */
48
+ export async function discard(spawn, root, target) {
49
+ if (target.tracked.length > 0) {
50
+ await mustGit(spawn, root, ['checkout', '-q', '--', ...target.tracked], '撤销更改失败');
51
+ }
52
+ if (target.untracked.length > 0) {
53
+ await mustGit(spawn, root, ['clean', '-f', '-q', '--', ...target.untracked], '删除未跟踪文件失败');
54
+ }
55
+ }
56
+ /**
57
+ * Commit exactly the selected files, as they are in the working tree. Files that were staged
58
+ * but not selected stay staged for a later commit, which spares the user the unstage-and-
59
+ * confirm step a commit of the whole index would need.
60
+ * @param spawn - the subprocess service's spawn.
61
+ * @param root - the repository's top level.
62
+ * @param change - the message and the files to commit.
63
+ */
64
+ export async function commit(spawn, root, change) {
65
+ if (change.message.trim() === '')
66
+ throw new GitError('请填写提交信息');
67
+ if (change.paths.length === 0)
68
+ throw new GitError('请选择要提交的文件');
69
+ // `commit -- <paths>` only takes files git already knows, so new files are added first.
70
+ await stage(spawn, root, change.paths);
71
+ await mustGit(spawn, root, ['commit', '-q', '-F', '-', '--', ...change.paths], {
72
+ failure: '提交失败',
73
+ input: change.message,
74
+ });
75
+ }
76
+ async function hasOrigin(spawn, root) {
77
+ const result = await runGit(spawn, root, ['remote']);
78
+ return result.stdout.split('\n').includes('origin');
79
+ }
80
+ /**
81
+ * Push the current branch. A branch without an upstream is pushed to origin and tracks it.
82
+ * @param spawn - the subprocess service's spawn.
83
+ * @param root - the repository's top level.
84
+ */
85
+ export async function push(spawn, root) {
86
+ const signal = AbortSignal.timeout(NETWORK_TIMEOUT_MS);
87
+ const upstream = await runGit(spawn, root, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}']);
88
+ if (upstream.exitCode === 0) {
89
+ await mustGit(spawn, root, ['push'], { failure: '推送失败', signal });
90
+ return;
91
+ }
92
+ if (!(await hasOrigin(spawn, root)))
93
+ throw new GitError(`推送失败:${NO_ORIGIN}`);
94
+ await mustGit(spawn, root, ['push', '-u', 'origin', 'HEAD'], { failure: '推送失败', signal });
95
+ }
96
+ /**
97
+ * Pull the current branch, fast-forward only: a merge or a rebase needs decisions the panel
98
+ * cannot ask for, so those are left to the terminal.
99
+ * @param spawn - the subprocess service's spawn.
100
+ * @param root - the repository's top level.
101
+ */
102
+ export async function pull(spawn, root) {
103
+ const signal = AbortSignal.timeout(NETWORK_TIMEOUT_MS);
104
+ await mustGit(spawn, root, ['pull', '--ff-only'], { failure: '拉取失败', signal });
105
+ }
106
+ async function validBranch(spawn, root, name) {
107
+ const result = await runGit(spawn, root, ['check-ref-format', '--branch', name]);
108
+ if (name.startsWith('-') || result.exitCode !== 0)
109
+ throw new GitError(`分支名称无效:${name}`);
110
+ }
111
+ /**
112
+ * Switch to a branch. A remote branch is checked out as a local branch that tracks it,
113
+ * unless a local branch of that name exists already.
114
+ * @param spawn - the subprocess service's spawn.
115
+ * @param root - the repository's top level.
116
+ * @param branch - the branch, and whether it names a remote branch such as `origin/main`.
117
+ */
118
+ export async function checkout(spawn, root, branch) {
119
+ await validBranch(spawn, root, branch.name);
120
+ const local = branch.remote ? branch.name.slice(branch.name.indexOf('/') + 1) : branch.name;
121
+ const exists = await runGit(spawn, root, ['show-ref', '--verify', '--quiet', `refs/heads/${local}`]);
122
+ const args = branch.remote && exists.exitCode !== 0 ? ['switch', '--track', branch.name] : ['switch', local];
123
+ await mustGit(spawn, root, args, '切换分支失败');
124
+ }
125
+ /**
126
+ * Create a branch from HEAD and switch to it.
127
+ * @param spawn - the subprocess service's spawn.
128
+ * @param root - the repository's top level.
129
+ * @param name - the new branch's name.
130
+ */
131
+ export async function createBranch(spawn, root, name) {
132
+ await validBranch(spawn, root, name);
133
+ await mustGit(spawn, root, ['switch', '-c', name], '新建分支失败');
134
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Host plugin: registers each configured CLI as a model provider and wires the
3
+ * session registry to the harness.
4
+ * @module dsh-ccgui
5
+ */
6
+ import type { Context } from '@deepseek-ai/cordis';
7
+ import { Service } from '@deepseek-ai/cordis';
8
+ import z from '@deepseek-ai/schemastery';
9
+ import type { ImageRef } from './prompt-images.ts';
10
+ import type { GitConnection, GitWebServer } from './git/git-routes.ts';
11
+ import type { CliAgentConfig } from './session-registry.ts';
12
+ export type { CliAgentConfig } from './session-registry.ts';
13
+ declare module '@deepseek-ai/cordis' {
14
+ interface Context {
15
+ cliBridge: CliBridge;
16
+ /** Provided by the harness's boot layer. */
17
+ dshHomePath?: (...segments: string[]) => string;
18
+ /**
19
+ * The harness's attachment store (`@deepseek-ai/dsh-attachment`), narrowed to the one
20
+ * call this plugin makes so the package does not have to depend on it.
21
+ */
22
+ attachments?: {
23
+ readImage(ref: ImageRef, signal?: AbortSignal): Promise<{
24
+ data: Uint8Array;
25
+ }>;
26
+ };
27
+ /** The harness's web server and its trust check, narrowed to what the git routes use. */
28
+ webServer: GitWebServer;
29
+ connection: GitConnection;
30
+ }
31
+ }
32
+ /** Plugin configuration. */
33
+ export interface Config {
34
+ agents: CliAgentConfig[];
35
+ /** Signal-escalation grace when stopping a CLI, in milliseconds. */
36
+ disposeGraceMs: number;
37
+ /** Time after stdin EOF for a CLI to exit by itself, in milliseconds. */
38
+ disposeEofGraceMs: number;
39
+ /** Time a cancelled CLI may take to end its turn before it is abandoned, in milliseconds. */
40
+ cancelGraceMs: number;
41
+ /** Time a CLI may take to start and accept a session, which must allow for a cold `npx` download. */
42
+ startTimeoutMs: number;
43
+ }
44
+ /** Service behind `ctx.cliBridge`. */
45
+ export default class CliBridge extends Service {
46
+ private readonly config;
47
+ static inject: string[];
48
+ static Config: z<Config>;
49
+ private readonly registry;
50
+ private readonly catalog;
51
+ /** Ends any model probe still running when this plugin goes away. */
52
+ private readonly probes;
53
+ constructor(ctx: Context, config: Config);
54
+ /** Read in the background: the menu works without it, and fills in once it arrives. */
55
+ private restoreModels;
56
+ /** The harness's home, or `~/.dsh` when it does not say. */
57
+ private homePath;
58
+ /**
59
+ * The 变更 panel's git routes. Taken as optional dependencies: a harness without a web
60
+ * server (a headless run) still gets the CLIs, just not the panel's backend.
61
+ */
62
+ private serveGit;
63
+ private registerRoutes;
64
+ private followSessions;
65
+ private ask;
66
+ private openTransport;
67
+ /**
68
+ * The temporary session goes in a directory of its own, away from the user's projects.
69
+ * Some CLIs keep a folder per working directory even for a deleted session, so this one
70
+ * never changes: a new directory on every start would add a folder each time. It is under
71
+ * the harness's home rather than the system temp directory, where on a shared machine
72
+ * another user could plant a project config at a predictable path for a CLI to load.
73
+ */
74
+ private probeProvider;
75
+ /** Looked up on each read: the attachment service can come and go while the plugin runs. */
76
+ private readImage;
77
+ private createSession;
78
+ }