@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/LICENSE +201 -0
- package/README.md +119 -2
- package/cordis.patch.yml +3 -0
- package/icon.svg +26 -0
- package/lib/client.js +2943 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +1274 -0
- package/package.json +91 -4
- package/src/client/GitBody.module.css +1283 -0
- package/src/client/GitBody.tsx +1496 -0
- package/src/client/GitBranchPill.module.css +292 -0
- package/src/client/GitBranchPill.tsx +368 -0
- package/src/client/GitTitle.tsx +27 -0
- package/src/client/definition.tsx +63 -0
- package/src/client/git-helpers.ts +351 -0
- package/src/client/index.ts +221 -0
- package/src/client/locales.ts +183 -0
- package/src/css.d.ts +4 -0
- package/src/git-executor.ts +1025 -0
- package/src/index.ts +220 -0
- package/src/tool.ts +299 -0
- package/src/types.ts +187 -0
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
|
+
}
|