@civilization/dsh-git-workbench 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Host entry for the Git Workbench & source control plugin.
3
+ * Provides RPC endpoints for the Client right-sidebar viewer, status bar, and model tools for Agents.
4
+ */
5
+
6
+ import type { Context } from '@deepseek-ai/cordis'
7
+ import {
8
+ getGitAllBranches,
9
+ getGitCommitDetails,
10
+ getGitDiff,
11
+ getGitLog,
12
+ getGitStatus,
13
+ getGitTags,
14
+ gitCheckout,
15
+ gitCommit,
16
+ gitCreateTag,
17
+ gitDeleteTag,
18
+ gitDiscard,
19
+ gitFetch,
20
+ gitPull,
21
+ gitPush,
22
+ gitStage,
23
+ gitUnstage,
24
+ } from './git-executor.ts'
25
+ import { registerGitTool } from './tool.ts'
26
+
27
+ export type * from './types.ts'
28
+
29
+ /** Cordis plugin name */
30
+ export const name = '@civilization/dsh-git-workbench'
31
+
32
+ /** Required host services */
33
+ export const inject = ['connection', 'webServer']
34
+
35
+ interface HostServices {
36
+ connection: {
37
+ rpc: {
38
+ handle: (
39
+ channel: string,
40
+ handler: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<unknown>
41
+ ) => () => void
42
+ }
43
+ }
44
+ webServer?: {
45
+ register: (route: unknown) => () => void
46
+ }
47
+ sessions?: {
48
+ get: (id: string) => { header?: { cwd?: string } } | undefined
49
+ }
50
+ sandboxPolicy?: {
51
+ workspaceRoot?: string
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Apply the Git host plugin.
57
+ * Sets up authenticated RPC handlers under `/git/*` and registers the model tool.
58
+ * @param ctx - Context carrying the connection and optional session services.
59
+ */
60
+ export function apply(ctx: Context): void {
61
+ const host = ctx as any as HostServices
62
+ const resolveTargetDir = (sessionId?: string): string => {
63
+ if (sessionId) {
64
+ const liveCwd = host.sessions?.get(sessionId)?.header?.cwd
65
+ if (liveCwd) return liveCwd
66
+ }
67
+ const workspaceRoot = host.sandboxPolicy?.workspaceRoot
68
+ return workspaceRoot ?? process.cwd()
69
+ }
70
+
71
+ // Register dedicated /git RPC channel
72
+ host.connection.rpc.handle(
73
+ '/git',
74
+ async (endpoint: string, payload: unknown, _signal: AbortSignal) => {
75
+ const body = (typeof payload === 'object' && payload !== null ? payload : {}) as Record<string, unknown>
76
+ const sessionId = typeof body.sessionId === 'string' ? body.sessionId : undefined
77
+ const cwd = typeof body.path === 'string' ? body.path : resolveTargetDir(sessionId)
78
+
79
+ try {
80
+ if (endpoint === 'status') {
81
+ const status = await getGitStatus(cwd)
82
+ return { ok: true, value: status }
83
+ }
84
+
85
+ if (endpoint === 'log') {
86
+ const maxCount = typeof body.maxCount === 'number' ? body.maxCount : 50
87
+ const commits = await getGitLog(cwd, maxCount)
88
+ return { ok: true, value: commits }
89
+ }
90
+
91
+ if (endpoint === 'commitDetails') {
92
+ const commit = typeof body.commit === 'string' ? body.commit : ''
93
+ const details = await getGitCommitDetails(cwd, commit)
94
+ return { ok: true, value: details }
95
+ }
96
+
97
+ if (endpoint === 'diff') {
98
+ const path = typeof body.filePath === 'string' ? body.filePath : undefined
99
+ const staged = typeof body.staged === 'boolean' ? body.staged : false
100
+ const commit = typeof body.commit === 'string' ? body.commit : undefined
101
+ const diff = await getGitDiff(cwd, {
102
+ ...path !== undefined ? { path } : {},
103
+ staged,
104
+ ...commit !== undefined ? { commit } : {},
105
+ })
106
+ return { ok: true, value: diff }
107
+ }
108
+
109
+ if (endpoint === 'branches') {
110
+ const branches = await getGitAllBranches(cwd)
111
+ return { ok: true, value: branches }
112
+ }
113
+
114
+ if (endpoint === 'checkout') {
115
+ const branch = typeof body.branch === 'string' ? body.branch : ''
116
+ const createNew = typeof body.create === 'boolean' ? body.create : false
117
+ const result = await gitCheckout(cwd, branch, createNew)
118
+ return { ok: true, value: result }
119
+ }
120
+
121
+ if (endpoint === 'stage') {
122
+ const paths = Array.isArray(body.paths)
123
+ ? (body.paths as string[])
124
+ : body.paths === 'all'
125
+ ? 'all'
126
+ : typeof body.path === 'string'
127
+ ? [body.path]
128
+ : 'all'
129
+ const result = await gitStage(cwd, paths)
130
+ return { ok: true, value: result }
131
+ }
132
+
133
+ if (endpoint === 'unstage') {
134
+ const paths = Array.isArray(body.paths)
135
+ ? (body.paths as string[])
136
+ : body.paths === 'all'
137
+ ? 'all'
138
+ : typeof body.path === 'string'
139
+ ? [body.path]
140
+ : 'all'
141
+ const result = await gitUnstage(cwd, paths)
142
+ return { ok: true, value: result }
143
+ }
144
+
145
+ if (endpoint === 'discard') {
146
+ const paths = Array.isArray(body.paths)
147
+ ? (body.paths as string[])
148
+ : typeof body.path === 'string'
149
+ ? [body.path]
150
+ : []
151
+ const result = await gitDiscard(cwd, paths)
152
+ return { ok: true, value: result }
153
+ }
154
+
155
+ if (endpoint === 'commit') {
156
+ const message = typeof body.message === 'string' ? body.message : ''
157
+ const result = await gitCommit(cwd, message)
158
+ return { ok: true, value: result }
159
+ }
160
+
161
+ if (endpoint === 'fetch') {
162
+ const result = await gitFetch(cwd)
163
+ return { ok: true, value: result }
164
+ }
165
+
166
+ if (endpoint === 'pull') {
167
+ const result = await gitPull(cwd)
168
+ return { ok: true, value: result }
169
+ }
170
+
171
+ if (endpoint === 'push') {
172
+ const result = await gitPush(cwd)
173
+ return { ok: true, value: result }
174
+ }
175
+
176
+ if (endpoint === 'tags') {
177
+ const tags = await getGitTags(cwd)
178
+ return { ok: true, value: tags }
179
+ }
180
+
181
+ if (endpoint === 'createTag') {
182
+ const name = typeof body.name === 'string' ? body.name : ''
183
+ const commit = typeof body.commit === 'string' ? body.commit : undefined
184
+ const message = typeof body.message === 'string' ? body.message : undefined
185
+ const result = await gitCreateTag(cwd, name, commit, message)
186
+ return { ok: true, value: result }
187
+ }
188
+
189
+ if (endpoint === 'deleteTag') {
190
+ const name = typeof body.name === 'string' ? body.name : ''
191
+ const result = await gitDeleteTag(cwd, name)
192
+ return { ok: true, value: result }
193
+ }
194
+
195
+
196
+ return {
197
+ ok: false,
198
+ error: {
199
+ code: 'git/not_found',
200
+ message: `Unknown git RPC endpoint: ${endpoint}`,
201
+ details: {},
202
+ },
203
+ }
204
+ } catch (error) {
205
+ return {
206
+ ok: false,
207
+ error: {
208
+ code: 'git/error',
209
+ message: error instanceof Error ? error.message : String(error),
210
+ details: {},
211
+ },
212
+ }
213
+ }
214
+ },
215
+ )
216
+
217
+
218
+ // Register the agent tool
219
+ registerGitTool(ctx, resolveTargetDir)
220
+ }
package/src/tool.ts ADDED
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Agent model tool for inspecting Git repository status, commits, and diffs.
3
+ */
4
+
5
+ import type { Context } from '@deepseek-ai/cordis'
6
+ import { defineTool } from '@deepseek-ai/dsh-tools'
7
+
8
+ export type JsonValue =
9
+ | string
10
+ | number
11
+ | boolean
12
+ | null
13
+ | { [key: string]: JsonValue }
14
+ | JsonValue[]
15
+ import {
16
+ getGitAllBranches,
17
+ getGitCommitDetails,
18
+ getGitDiff,
19
+ getGitLog,
20
+ getGitStatus,
21
+ getGitTags,
22
+ gitCheckout,
23
+ gitCreateTag,
24
+ gitDeleteTag,
25
+ } from './git-executor.ts'
26
+
27
+ /**
28
+ * Register git model tools (git_view and git_checkout) in the tools service.
29
+ * @param ctx - Context carrying the tools service.
30
+ * @param resolveDir - Resolver for the current agent or session working directory.
31
+ */
32
+ export function registerGitTool(ctx: Context, resolveDir: (sessionId?: string) => string): void {
33
+ ctx.inject(['tools'], (scope) => {
34
+ // 1. Tool for inspecting git status, DAG history, diff, and branches
35
+ scope.effect(() => scope.tools.register(defineTool({
36
+ name: 'git_view',
37
+ description: 'Inspect the current project Git repository status, changed files, DAG commit tree, branch list, or code diffs.',
38
+ parameters: {
39
+ action: {
40
+ type: 'string',
41
+ enum: ['status', 'log', 'diff', 'branches', 'commit', 'tags'],
42
+ description: 'The Git query action: "status" (changes and branch info), "log" (commits with DAG topology rails), "diff" (code diffs), "branches" (branch list), "commit" (commit details), or "tags" (all repository tags).',
43
+ },
44
+ path: {
45
+ type: 'string',
46
+ description: 'Optional file path filter for diff or status inspection.',
47
+ },
48
+ staged: {
49
+ type: 'boolean',
50
+ description: 'Whether to check staged changes when viewing diff (defaults to false for working tree changes).',
51
+ },
52
+ maxCount: {
53
+ type: 'number',
54
+ description: 'Maximum number of commits to retrieve when action is "log" (defaults to 30).',
55
+ },
56
+ commit: {
57
+ type: 'string',
58
+ description: 'Specific commit hash to inspect diff or details for.',
59
+ },
60
+ },
61
+ output: {
62
+ schema: {
63
+ type: 'object',
64
+ additionalProperties: true,
65
+ },
66
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
67
+ },
68
+ async execute(args, exec): Promise<Record<string, JsonValue>> {
69
+ const cwd = resolveDir(exec.agent?.session.id)
70
+ const action = args.action ?? 'status'
71
+
72
+ switch (action) {
73
+ case 'log': {
74
+ const rawCommits = await getGitLog(cwd, args.maxCount ?? 30)
75
+ const commits = rawCommits.map(c => ({
76
+ hash: c.hash,
77
+ shortHash: c.shortHash,
78
+ author: c.author,
79
+ authorEmail: c.authorEmail,
80
+ relativeDate: c.relativeDate,
81
+ date: c.date,
82
+ subject: c.subject,
83
+ parents: c.parents ? [...c.parents] : [],
84
+ refs: c.refs ? [...c.refs] : [],
85
+ ...c.lane !== undefined ? { lane: c.lane } : {},
86
+ ...c.laneColor !== undefined ? { laneColor: c.laneColor } : {},
87
+ }))
88
+ return { action: 'log', count: commits.length, commits }
89
+ }
90
+ case 'commit': {
91
+ if (!args.commit) {
92
+ return { action: 'commit', error: 'Missing commit parameter' }
93
+ }
94
+ const details = await getGitCommitDetails(cwd, args.commit)
95
+ if (!details) return { action: 'commit', found: false }
96
+ return {
97
+ action: 'commit',
98
+ found: true,
99
+ commit: {
100
+ hash: details.commit.hash,
101
+ shortHash: details.commit.shortHash,
102
+ author: details.commit.author,
103
+ date: details.commit.date,
104
+ subject: details.commit.subject,
105
+ },
106
+ files: details.files.map(f => ({
107
+ path: f.path,
108
+ status: f.status,
109
+ additions: f.additions,
110
+ deletions: f.deletions,
111
+ })),
112
+ totalAdditions: details.totalAdditions,
113
+ totalDeletions: details.totalDeletions,
114
+ }
115
+ }
116
+ case 'diff': {
117
+ const diff = await getGitDiff(cwd, {
118
+ ...args.path !== undefined ? { path: args.path } : {},
119
+ ...args.staged !== undefined ? { staged: args.staged } : {},
120
+ ...args.commit !== undefined ? { commit: args.commit } : {},
121
+ })
122
+ const files = diff.files.map(f => ({ path: f.path, additions: f.additions, deletions: f.deletions }))
123
+ return { action: 'diff', files, rawDiff: diff.rawDiff }
124
+ }
125
+ case 'branches': {
126
+ const summary = await getGitAllBranches(cwd)
127
+ return {
128
+ action: 'branches',
129
+ ...summary.current !== undefined ? { current: summary.current } : {},
130
+ local: summary.local.map(b => ({
131
+ name: b.name,
132
+ isCurrent: b.isCurrent,
133
+ ...b.upstream !== undefined ? { upstream: b.upstream } : {},
134
+ ...b.ahead !== undefined ? { ahead: b.ahead } : {},
135
+ ...b.behind !== undefined ? { behind: b.behind } : {},
136
+ })),
137
+ remote: summary.remote.map(b => ({
138
+ name: b.name,
139
+ isCurrent: b.isCurrent,
140
+ })),
141
+ }
142
+ }
143
+ case 'tags': {
144
+ const rawTags = await getGitTags(cwd)
145
+ return {
146
+ action: 'tags',
147
+ total: rawTags.length,
148
+ tags: rawTags.map(t => ({
149
+ name: t.name,
150
+ commitHash: t.commitHash,
151
+ shortHash: t.shortHash,
152
+ date: t.date,
153
+ relativeDate: t.relativeDate,
154
+ message: t.message,
155
+ author: t.author,
156
+ isAnnotated: t.isAnnotated,
157
+ })),
158
+ }
159
+ }
160
+ case 'status':
161
+ default: {
162
+ const status = await getGitStatus(cwd)
163
+ return {
164
+ action: 'status',
165
+ isRepo: status.isRepo,
166
+ ...status.rootPath !== undefined ? { rootPath: status.rootPath } : {},
167
+ ...status.branch !== undefined ? { branch: status.branch } : {},
168
+ ...status.upstream !== undefined ? { upstream: status.upstream } : {},
169
+ ahead: status.ahead,
170
+ behind: status.behind,
171
+ staged: status.staged.map(f => ({ path: f.path, status: f.status, staged: f.staged })),
172
+ unstaged: status.unstaged.map(f => ({ path: f.path, status: f.status, staged: f.staged })),
173
+ untracked: status.untracked.map(f => ({ path: f.path, status: f.status, staged: f.staged })),
174
+ totalChanges: status.totalChanges,
175
+ }
176
+ }
177
+ }
178
+ },
179
+ presentCall: args => ({ card: 'generic', title: `git_view (${args.action ?? 'status'})`, rawInput: args }),
180
+ })), 'experimental-git: register git_view tool')
181
+
182
+ // 2. Dedicated tool for checking out or creating branches
183
+ scope.effect(() => scope.tools.register(defineTool({
184
+ name: 'git_checkout',
185
+ description: 'Switch branch or create and checkout a new branch in the current project Git repository.',
186
+ parameters: {
187
+ branch: {
188
+ type: 'string',
189
+ required: true,
190
+ description: 'The target branch name to switch to (e.g. "feature/auth", "dev", "master").',
191
+ },
192
+ create: {
193
+ type: 'boolean',
194
+ description: 'Whether to create a new branch from current HEAD and check it out (-b). Defaults to false.',
195
+ },
196
+ },
197
+ output: {
198
+ schema: {
199
+ type: 'object',
200
+ additionalProperties: true,
201
+ },
202
+ render: (_args, val) => {
203
+ const value = val as { success?: boolean; branch?: string; message?: string }
204
+ return [{
205
+ type: 'text',
206
+ text: value.success
207
+ ? `Successfully checked out branch '${value.branch ?? ''}'.`
208
+ : `Failed to checkout branch '${value.branch ?? ''}': ${value.message ?? ''}`,
209
+ }]
210
+ },
211
+ },
212
+
213
+ async execute(args, exec): Promise<Record<string, JsonValue>> {
214
+ const cwd = resolveDir(exec.agent?.session.id)
215
+ const branch = args.branch.trim()
216
+ const createNew = args.create ?? false
217
+ const result = await gitCheckout(cwd, branch, createNew)
218
+
219
+ return {
220
+ success: result.success,
221
+ branch,
222
+ message: result.message ?? '',
223
+ }
224
+ },
225
+ presentCall: args => ({
226
+ card: 'generic',
227
+ title: `git_checkout ${args.create ? '-b ' : ''}${args.branch}`,
228
+ rawInput: args,
229
+ }),
230
+ })), 'experimental-git: register git_checkout tool')
231
+
232
+ // 3. Dedicated tool for creating or deleting git tags
233
+ scope.effect(() => scope.tools.register(defineTool({
234
+ name: 'git_tag',
235
+ description: 'Create or delete a Git tag in the current project repository.',
236
+ parameters: {
237
+ name: {
238
+ type: 'string',
239
+ required: true,
240
+ description: 'The tag name (e.g. "v1.0.0", "release-20261020").',
241
+ },
242
+ action: {
243
+ type: 'string',
244
+ enum: ['create', 'delete'],
245
+ description: 'Tag operation: "create" (default) or "delete".',
246
+ },
247
+ commit: {
248
+ type: 'string',
249
+ description: 'Target commit hash to place the tag on when creating (defaults to current HEAD).',
250
+ },
251
+ message: {
252
+ type: 'string',
253
+ description: 'Optional annotation message for an annotated tag (git tag -a -m).',
254
+ },
255
+ },
256
+ output: {
257
+ schema: {
258
+ type: 'object',
259
+ additionalProperties: true,
260
+ },
261
+ render: (_args, val) => {
262
+ const value = val as { success?: boolean; name?: string; message?: string }
263
+ return [{
264
+ type: 'text',
265
+ text: value.message ?? (value.success ? 'Success' : 'Failed'),
266
+ }]
267
+ },
268
+ },
269
+ async execute(args, exec): Promise<Record<string, JsonValue>> {
270
+ const cwd = resolveDir(exec.agent?.session.id)
271
+ const name = args.name.trim()
272
+ const action = args.action ?? 'create'
273
+
274
+ if (action === 'delete') {
275
+ const result = await gitDeleteTag(cwd, name)
276
+ return {
277
+ success: result.success,
278
+ name,
279
+ action: 'delete',
280
+ message: result.message ?? '',
281
+ }
282
+ }
283
+
284
+ const result = await gitCreateTag(cwd, name, args.commit, args.message)
285
+ return {
286
+ success: result.success,
287
+ name,
288
+ action: 'create',
289
+ message: result.message ?? '',
290
+ }
291
+ },
292
+ presentCall: args => ({
293
+ card: 'generic',
294
+ title: `git_tag ${args.action ?? 'create'} ${args.name}`,
295
+ rawInput: args,
296
+ }),
297
+ })), 'experimental-git: register git_tag tool')
298
+ })
299
+ }
package/src/types.ts ADDED
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Core type declarations for the experimental Git plugin.
3
+ * Shared across the Host RPC handlers and Client UI viewers.
4
+ */
5
+
6
+ /** File change status representation in git */
7
+ export type GitFileStatusCode = 'modified' | 'added' | 'deleted' | 'renamed' | 'untracked' | 'copied'
8
+
9
+ /** One changed file entry in the working directory or staging area */
10
+ export interface GitFileStatus {
11
+ /** Relative file path from the repository root */
12
+ readonly path: string
13
+ /** Previous path if the file was renamed */
14
+ readonly originalPath?: string
15
+ /** Normalized status kind */
16
+ readonly status: GitFileStatusCode
17
+ /** Whether the change has been staged to index */
18
+ readonly staged: boolean
19
+ }
20
+
21
+ /** Information about one local git branch */
22
+ export interface GitBranchInfo {
23
+ /** Branch name (e.g. 'main', 'master', 'feature/xyz') */
24
+ readonly name: string
25
+ /** Whether this branch is currently checked out */
26
+ readonly isCurrent: boolean
27
+ /** Upstream tracking branch if configured */
28
+ readonly upstream?: string
29
+ /** Number of commits ahead of upstream */
30
+ readonly ahead?: number
31
+ /** Number of commits behind upstream */
32
+ readonly behind?: number
33
+ }
34
+
35
+ /** Full status summary of the workspace repository */
36
+ export interface GitStatusSummary {
37
+ /** Whether the checked directory is inside a Git worktree/repository */
38
+ readonly isRepo: boolean
39
+ /** Absolute root directory of the repository */
40
+ readonly rootPath?: string
41
+ /** Current branch name, or commit hash if detached */
42
+ readonly branch?: string
43
+ /** Upstream branch name */
44
+ readonly upstream?: string
45
+ /** Commits ahead of upstream */
46
+ readonly ahead: number
47
+ /** Commits behind upstream */
48
+ readonly behind: number
49
+ /** Latest commit summary on current HEAD */
50
+ readonly headCommit?: {
51
+ readonly hash: string
52
+ readonly shortHash: string
53
+ readonly message: string
54
+ }
55
+ /** Staged changes in index */
56
+ readonly staged: readonly GitFileStatus[]
57
+ /** Unstaged changes in working tree */
58
+ readonly unstaged: readonly GitFileStatus[]
59
+ /** Untracked new files */
60
+ readonly untracked: readonly GitFileStatus[]
61
+ /** Total count of all changed/untracked files */
62
+ readonly totalChanges: number
63
+ /** Total count of commits in the current repository */
64
+ readonly totalCommitsCount?: number
65
+ }
66
+
67
+ /** One commit log entry with optional DAG topology metadata */
68
+ export interface GitCommitItem {
69
+ /** Full 40-character commit hash */
70
+ readonly hash: string
71
+ /** Abbreviated 7-character commit hash */
72
+ readonly shortHash: string
73
+ /** Author name */
74
+ readonly author: string
75
+ /** Author email address */
76
+ readonly authorEmail: string
77
+ /** Human-readable relative date (e.g. '2 hours ago') */
78
+ readonly relativeDate: string
79
+ /** ISO date string */
80
+ readonly date: string
81
+ /** Commit subject/first line of commit message */
82
+ readonly subject: string
83
+ /** Full multiline commit body message (excluding subject) */
84
+ readonly body?: string
85
+ /** Parent commit hashes */
86
+ readonly parents?: readonly string[]
87
+ /** Git branch / tag reference names */
88
+ readonly refs?: readonly string[]
89
+ /** Topological vertical lane column index (0-based) */
90
+ readonly lane?: number
91
+ /** Color hex code for the current commit's lane */
92
+ readonly laneColor?: string
93
+ /** Connecting graph rails to parent commits */
94
+ readonly paths?: readonly GitGraphLanePath[]
95
+ }
96
+
97
+ /** Vector path connecting lanes in the DAG graph visualization */
98
+ export interface GitGraphLanePath {
99
+ /** Starting lane index (from this commit) */
100
+ readonly fromLane: number
101
+ /** Target lane index (to parent commit or pass-through) */
102
+ readonly toLane: number
103
+ /** Type of track: pass-through, fork out, or merge in */
104
+ readonly kind: 'straight' | 'fork' | 'merge'
105
+ /** Color of this track line */
106
+ readonly color: string
107
+ }
108
+
109
+ /** Detail information for a single commit including changed files */
110
+ export interface GitCommitDetail {
111
+ readonly commit: GitCommitItem
112
+ readonly body?: string
113
+ readonly parents?: readonly string[]
114
+ readonly files: readonly {
115
+ readonly path: string
116
+ readonly oldPath?: string
117
+ readonly status: GitFileStatusCode
118
+ readonly additions: number
119
+ readonly deletions: number
120
+ }[]
121
+ readonly totalAdditions: number
122
+ readonly totalDeletions: number
123
+ }
124
+
125
+ /** Complete log query summary including commits and total count */
126
+ export interface GitLogSummary {
127
+ readonly commits: readonly GitCommitItem[]
128
+ readonly totalCount: number
129
+ }
130
+
131
+ /** One Git tag entry */
132
+ export interface GitTagItem {
133
+ /** Tag name (e.g. 'v1.0.0', 'dsh-v0.2.1-rc.2') */
134
+ readonly name: string
135
+ /** Target commit full 40-character SHA hash */
136
+ readonly commitHash: string
137
+ /** Target commit abbreviated 7-character hash */
138
+ readonly shortHash: string
139
+ /** ISO 8601 date string of tag or commit */
140
+ readonly date: string
141
+ /** Human-readable relative date (e.g. '3 days ago') */
142
+ readonly relativeDate: string
143
+ /** Tag annotation message or commit subject */
144
+ readonly message: string
145
+ /** Author or tagger name */
146
+ readonly author: string
147
+ /** Whether this is an annotated tag (git tag -a) */
148
+ readonly isAnnotated: boolean
149
+ }
150
+
151
+ /** Aggregated local and remote branches */
152
+ export interface GitBranchesSummary {
153
+ readonly current?: string
154
+ readonly local: readonly GitBranchInfo[]
155
+ readonly remote: readonly GitBranchInfo[]
156
+ }
157
+
158
+ /** Generic result of executing a git mutative operation */
159
+ export interface GitOperationResult {
160
+ readonly success: boolean
161
+ readonly message?: string
162
+ readonly output?: string
163
+ }
164
+
165
+ /** One file's diff result */
166
+ export interface GitDiffFile {
167
+ /** File path */
168
+ readonly path: string
169
+ /** Old file path if renamed */
170
+ readonly oldPath?: string
171
+ /** Change status */
172
+ readonly status: GitFileStatusCode
173
+ /** Raw unified diff patch lines */
174
+ readonly diffText: string
175
+ /** Count of added lines */
176
+ readonly additions: number
177
+ /** Count of removed lines */
178
+ readonly deletions: number
179
+ }
180
+
181
+ /** Complete diff result summary */
182
+ export interface GitDiffSummary {
183
+ /** List of parsed diff files */
184
+ readonly files: readonly GitDiffFile[]
185
+ /** Complete raw unified diff output */
186
+ readonly rawDiff: string
187
+ }