@civilization/dsh-git-workbench 0.4.0 → 0.4.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 (3) hide show
  1. package/lib/index.js +199 -102
  2. package/package.json +1 -1
  3. package/src/index.ts +233 -149
package/lib/index.js CHANGED
@@ -1157,118 +1157,215 @@ function registerGitTool(ctx, resolveDir) {
1157
1157
  /** Cordis plugin name */
1158
1158
  const name = "@civilization/dsh-git-workbench";
1159
1159
  /** Required host services */
1160
- const inject = ["connection", "webServer"];
1160
+ const inject = [
1161
+ "webServer",
1162
+ "sessions",
1163
+ "sandboxPolicy"
1164
+ ];
1161
1165
  /**
1162
- * Apply the Git host plugin.
1163
- * Sets up authenticated RPC handlers under `/git/*` and registers the model tool.
1164
- * @param ctx - Context carrying the connection and optional session services.
1166
+ * Execute Git operations based on requested endpoint and payload.
1165
1167
  */
1166
- function apply(ctx) {
1167
- const host = ctx;
1168
- const resolveTargetDir = (sessionId) => {
1169
- if (sessionId) {
1170
- const liveCwd = host.sessions?.get(sessionId)?.header?.cwd;
1171
- if (liveCwd) return liveCwd;
1172
- }
1173
- return host.sandboxPolicy?.workspaceRoot ?? process.cwd();
1174
- };
1175
- host.connection.rpc.handle("/git", async (endpoint, payload, _signal) => {
1168
+ async function executeGitEndpoint(endpoint, payload, resolveTargetDir) {
1169
+ try {
1176
1170
  const body = typeof payload === "object" && payload !== null ? payload : {};
1177
1171
  const sessionId = typeof body.sessionId === "string" ? body.sessionId : void 0;
1178
1172
  const cwd = typeof body.path === "string" ? body.path : resolveTargetDir(sessionId);
1179
- try {
1180
- if (endpoint === "status") return {
1181
- ok: true,
1182
- value: await getGitStatus(cwd)
1183
- };
1184
- if (endpoint === "log") return {
1185
- ok: true,
1186
- value: await getGitLog(cwd, typeof body.maxCount === "number" ? body.maxCount : 50)
1187
- };
1188
- if (endpoint === "commitDetails") return {
1173
+ if (endpoint === "status") return {
1174
+ ok: true,
1175
+ value: await getGitStatus(cwd)
1176
+ };
1177
+ if (endpoint === "log") return {
1178
+ ok: true,
1179
+ value: await getGitLog(cwd, typeof body.maxCount === "number" ? body.maxCount : 50)
1180
+ };
1181
+ if (endpoint === "commitDetails") return {
1182
+ ok: true,
1183
+ value: await getGitCommitDetails(cwd, typeof body.commit === "string" ? body.commit : "")
1184
+ };
1185
+ if (endpoint === "diff") {
1186
+ const path = typeof body.filePath === "string" ? body.filePath : void 0;
1187
+ const staged = typeof body.staged === "boolean" ? body.staged : false;
1188
+ const commit = typeof body.commit === "string" ? body.commit : void 0;
1189
+ return {
1189
1190
  ok: true,
1190
- value: await getGitCommitDetails(cwd, typeof body.commit === "string" ? body.commit : "")
1191
+ value: await getGitDiff(cwd, {
1192
+ ...path !== void 0 ? { path } : {},
1193
+ staged,
1194
+ ...commit !== void 0 ? { commit } : {}
1195
+ })
1191
1196
  };
1192
- if (endpoint === "diff") {
1193
- const path = typeof body.filePath === "string" ? body.filePath : void 0;
1194
- const staged = typeof body.staged === "boolean" ? body.staged : false;
1195
- const commit = typeof body.commit === "string" ? body.commit : void 0;
1196
- return {
1197
- ok: true,
1198
- value: await getGitDiff(cwd, {
1199
- ...path !== void 0 ? { path } : {},
1200
- staged,
1201
- ...commit !== void 0 ? { commit } : {}
1202
- })
1203
- };
1197
+ }
1198
+ if (endpoint === "branches") return {
1199
+ ok: true,
1200
+ value: await getGitAllBranches(cwd)
1201
+ };
1202
+ if (endpoint === "checkout") return {
1203
+ ok: true,
1204
+ value: await gitCheckout(cwd, typeof body.branch === "string" ? body.branch : "", typeof body.create === "boolean" ? body.create : false)
1205
+ };
1206
+ if (endpoint === "stage") return {
1207
+ ok: true,
1208
+ value: await gitStage(cwd, Array.isArray(body.paths) ? body.paths : body.paths === "all" ? "all" : typeof body.path === "string" ? [body.path] : "all")
1209
+ };
1210
+ if (endpoint === "unstage") return {
1211
+ ok: true,
1212
+ value: await gitUnstage(cwd, Array.isArray(body.paths) ? body.paths : body.paths === "all" ? "all" : typeof body.path === "string" ? [body.path] : "all")
1213
+ };
1214
+ if (endpoint === "discard") return {
1215
+ ok: true,
1216
+ value: await gitDiscard(cwd, Array.isArray(body.paths) ? body.paths : typeof body.path === "string" ? [body.path] : [])
1217
+ };
1218
+ if (endpoint === "commit") return {
1219
+ ok: true,
1220
+ value: await gitCommit(cwd, typeof body.message === "string" ? body.message : "")
1221
+ };
1222
+ if (endpoint === "fetch") return {
1223
+ ok: true,
1224
+ value: await gitFetch(cwd)
1225
+ };
1226
+ if (endpoint === "pull") return {
1227
+ ok: true,
1228
+ value: await gitPull(cwd)
1229
+ };
1230
+ if (endpoint === "push") return {
1231
+ ok: true,
1232
+ value: await gitPush(cwd)
1233
+ };
1234
+ if (endpoint === "tags") return {
1235
+ ok: true,
1236
+ value: await getGitTags(cwd)
1237
+ };
1238
+ if (endpoint === "createTag") return {
1239
+ ok: true,
1240
+ value: await gitCreateTag(cwd, typeof body.name === "string" ? body.name : "", typeof body.commit === "string" ? body.commit : void 0, typeof body.message === "string" ? body.message : void 0)
1241
+ };
1242
+ if (endpoint === "deleteTag") return {
1243
+ ok: true,
1244
+ value: await gitDeleteTag(cwd, typeof body.name === "string" ? body.name : "")
1245
+ };
1246
+ return {
1247
+ ok: false,
1248
+ error: {
1249
+ code: "git/not_found",
1250
+ message: `Unknown git RPC endpoint: ${endpoint}`,
1251
+ details: {}
1204
1252
  }
1205
- if (endpoint === "branches") return {
1206
- ok: true,
1207
- value: await getGitAllBranches(cwd)
1208
- };
1209
- if (endpoint === "checkout") return {
1210
- ok: true,
1211
- value: await gitCheckout(cwd, typeof body.branch === "string" ? body.branch : "", typeof body.create === "boolean" ? body.create : false)
1212
- };
1213
- if (endpoint === "stage") return {
1214
- ok: true,
1215
- value: await gitStage(cwd, Array.isArray(body.paths) ? body.paths : body.paths === "all" ? "all" : typeof body.path === "string" ? [body.path] : "all")
1216
- };
1217
- if (endpoint === "unstage") return {
1218
- ok: true,
1219
- value: await gitUnstage(cwd, Array.isArray(body.paths) ? body.paths : body.paths === "all" ? "all" : typeof body.path === "string" ? [body.path] : "all")
1220
- };
1221
- if (endpoint === "discard") return {
1222
- ok: true,
1223
- value: await gitDiscard(cwd, Array.isArray(body.paths) ? body.paths : typeof body.path === "string" ? [body.path] : [])
1224
- };
1225
- if (endpoint === "commit") return {
1226
- ok: true,
1227
- value: await gitCommit(cwd, typeof body.message === "string" ? body.message : "")
1228
- };
1229
- if (endpoint === "fetch") return {
1230
- ok: true,
1231
- value: await gitFetch(cwd)
1232
- };
1233
- if (endpoint === "pull") return {
1234
- ok: true,
1235
- value: await gitPull(cwd)
1236
- };
1237
- if (endpoint === "push") return {
1238
- ok: true,
1239
- value: await gitPush(cwd)
1240
- };
1241
- if (endpoint === "tags") return {
1242
- ok: true,
1243
- value: await getGitTags(cwd)
1244
- };
1245
- if (endpoint === "createTag") return {
1246
- ok: true,
1247
- value: await gitCreateTag(cwd, typeof body.name === "string" ? body.name : "", typeof body.commit === "string" ? body.commit : void 0, typeof body.message === "string" ? body.message : void 0)
1248
- };
1249
- if (endpoint === "deleteTag") return {
1250
- ok: true,
1251
- value: await gitDeleteTag(cwd, typeof body.name === "string" ? body.name : "")
1252
- };
1253
- return {
1254
- ok: false,
1255
- error: {
1256
- code: "git/not_found",
1257
- message: `Unknown git RPC endpoint: ${endpoint}`,
1258
- details: {}
1253
+ };
1254
+ } catch (error) {
1255
+ return {
1256
+ ok: false,
1257
+ error: {
1258
+ code: "git/error",
1259
+ message: error instanceof Error ? error.message : String(error),
1260
+ details: {}
1261
+ }
1262
+ };
1263
+ }
1264
+ }
1265
+ /**
1266
+ * Apply the Git host plugin.
1267
+ * Sets up direct WebServer HTTP route handlers under `/git/*` and registers the model tool.
1268
+ * @param ctx - Context carrying the webServer, sessions, and sandboxPolicy services.
1269
+ */
1270
+ function apply(ctx) {
1271
+ const host = ctx;
1272
+ const resolveTargetDir = (sessionId) => {
1273
+ try {
1274
+ if (sessionId) {
1275
+ const liveCwd = host.sessions?.get?.(sessionId)?.header?.cwd;
1276
+ if (liveCwd) return liveCwd;
1277
+ }
1278
+ } catch {}
1279
+ try {
1280
+ const workspaceRoot = host.sandboxPolicy?.workspaceRoot;
1281
+ if (workspaceRoot) return workspaceRoot;
1282
+ } catch {}
1283
+ return process.cwd();
1284
+ };
1285
+ const webServer = ctx.webServer;
1286
+ if (webServer?.register) ctx.effect(() => webServer.register({
1287
+ kind: "prefix",
1288
+ path: "/git",
1289
+ handler: async (req, res) => {
1290
+ try {
1291
+ if (req.method === "OPTIONS") {
1292
+ res.writeHead(204, {
1293
+ "Access-Control-Allow-Origin": "*",
1294
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
1295
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
1296
+ });
1297
+ return res.end();
1259
1298
  }
1260
- };
1261
- } catch (error) {
1262
- return {
1263
- ok: false,
1264
- error: {
1265
- code: "git/error",
1266
- message: error instanceof Error ? error.message : String(error),
1267
- details: {}
1299
+ if (req.method !== "POST") {
1300
+ res.writeHead(405, { "Content-Type": "application/json" });
1301
+ return res.end(JSON.stringify({
1302
+ ok: false,
1303
+ error: { message: "Method Not Allowed" }
1304
+ }));
1268
1305
  }
1269
- };
1306
+ const match = new URL(req.url ?? "/", "http://localhost").pathname.match(/^\/git\/([a-zA-Z0-9_-]+)/);
1307
+ const endpoint = match ? match[1] : void 0;
1308
+ if (!endpoint) {
1309
+ res.writeHead(404, { "Content-Type": "application/json" });
1310
+ return res.end(JSON.stringify({
1311
+ ok: false,
1312
+ error: { message: "Not Found" }
1313
+ }));
1314
+ }
1315
+ let rawBody = "";
1316
+ req.on("data", (chunk) => {
1317
+ rawBody += chunk.toString("utf8");
1318
+ });
1319
+ req.on("end", async () => {
1320
+ try {
1321
+ let body = {};
1322
+ try {
1323
+ if (rawBody.trim()) body = JSON.parse(rawBody);
1324
+ } catch {
1325
+ res.writeHead(400, { "Content-Type": "application/json" });
1326
+ return res.end(JSON.stringify({
1327
+ ok: false,
1328
+ error: { message: "Invalid JSON body" }
1329
+ }));
1330
+ }
1331
+ const rpcId = body?.type === "client-request" ? body.rpcId : void 0;
1332
+ const payload = body?.type === "client-request" ? body.payload ?? {} : body;
1333
+ const result = await executeGitEndpoint(endpoint, payload, resolveTargetDir);
1334
+ res.writeHead(200, {
1335
+ "Content-Type": "application/json; charset=utf-8",
1336
+ "Access-Control-Allow-Origin": "*"
1337
+ });
1338
+ if (rpcId !== void 0) res.end(JSON.stringify({
1339
+ type: "server-response",
1340
+ rpcId,
1341
+ result
1342
+ }));
1343
+ else res.end(JSON.stringify(result));
1344
+ } catch (err) {
1345
+ res.writeHead(500, { "Content-Type": "application/json" });
1346
+ res.end(JSON.stringify({
1347
+ ok: false,
1348
+ error: {
1349
+ code: "git/internal_error",
1350
+ message: err?.message ?? String(err)
1351
+ }
1352
+ }));
1353
+ }
1354
+ });
1355
+ } catch (err) {
1356
+ try {
1357
+ res.writeHead(500, { "Content-Type": "application/json" });
1358
+ res.end(JSON.stringify({
1359
+ ok: false,
1360
+ error: {
1361
+ code: "git/internal_error",
1362
+ message: err?.message ?? String(err)
1363
+ }
1364
+ }));
1365
+ } catch {}
1366
+ }
1270
1367
  }
1271
- });
1368
+ }), "git-workbench: /git routes");
1272
1369
  registerGitTool(ctx, resolveTargetDir);
1273
1370
  }
1274
- export { apply, inject, name };
1371
+ export { apply, executeGitEndpoint, inject, name };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civilization/dsh-git-workbench",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "IntelliJ IDEA-grade Git Workbench, DAG topology graph, branch switcher, tag manager and agent tools for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
package/src/index.ts CHANGED
@@ -30,17 +30,9 @@ export type * from './types.ts'
30
30
  export const name = '@civilization/dsh-git-workbench'
31
31
 
32
32
  /** Required host services */
33
- export const inject = ['connection', 'webServer']
33
+ export const inject = ['webServer', 'sessions', 'sandboxPolicy']
34
34
 
35
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
36
  webServer?: {
45
37
  register: (route: unknown) => () => void
46
38
  }
@@ -53,168 +45,260 @@ interface HostServices {
53
45
  }
54
46
 
55
47
  /**
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.
48
+ * Execute Git operations based on requested endpoint and payload.
59
49
  */
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
50
+ export async function executeGitEndpoint(
51
+ endpoint: string,
52
+ payload: unknown,
53
+ resolveTargetDir: (sessionId?: string) => string,
54
+ ): Promise<{ ok: true; value: unknown } | { ok: false; error: { code: string; message: string; details: Record<string, unknown> } }> {
55
+ try {
56
+ const body = (typeof payload === 'object' && payload !== null ? payload : {}) as Record<string, unknown>
57
+ const sessionId = typeof body.sessionId === 'string' ? body.sessionId : undefined
58
+ const cwd = typeof body.path === 'string' ? body.path : resolveTargetDir(sessionId)
59
+
60
+ if (endpoint === 'status') {
61
+ const status = await getGitStatus(cwd)
62
+ return { ok: true, value: status }
66
63
  }
67
- const workspaceRoot = host.sandboxPolicy?.workspaceRoot
68
- return workspaceRoot ?? process.cwd()
69
- }
70
64
 
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
- }
65
+ if (endpoint === 'log') {
66
+ const maxCount = typeof body.maxCount === 'number' ? body.maxCount : 50
67
+ const commits = await getGitLog(cwd, maxCount)
68
+ return { ok: true, value: commits }
69
+ }
84
70
 
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
- }
71
+ if (endpoint === 'commitDetails') {
72
+ const commit = typeof body.commit === 'string' ? body.commit : ''
73
+ const details = await getGitCommitDetails(cwd, commit)
74
+ return { ok: true, value: details }
75
+ }
90
76
 
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
- }
77
+ if (endpoint === 'diff') {
78
+ const path = typeof body.filePath === 'string' ? body.filePath : undefined
79
+ const staged = typeof body.staged === 'boolean' ? body.staged : false
80
+ const commit = typeof body.commit === 'string' ? body.commit : undefined
81
+ const diff = await getGitDiff(cwd, {
82
+ ...path !== undefined ? { path } : {},
83
+ staged,
84
+ ...commit !== undefined ? { commit } : {},
85
+ })
86
+ return { ok: true, value: diff }
87
+ }
96
88
 
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
- }
89
+ if (endpoint === 'branches') {
90
+ const branches = await getGitAllBranches(cwd)
91
+ return { ok: true, value: branches }
92
+ }
108
93
 
109
- if (endpoint === 'branches') {
110
- const branches = await getGitAllBranches(cwd)
111
- return { ok: true, value: branches }
112
- }
94
+ if (endpoint === 'checkout') {
95
+ const branch = typeof body.branch === 'string' ? body.branch : ''
96
+ const createNew = typeof body.create === 'boolean' ? body.create : false
97
+ const result = await gitCheckout(cwd, branch, createNew)
98
+ return { ok: true, value: result }
99
+ }
113
100
 
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
- }
101
+ if (endpoint === 'stage') {
102
+ const paths = Array.isArray(body.paths)
103
+ ? (body.paths as string[])
104
+ : body.paths === 'all'
105
+ ? 'all'
106
+ : typeof body.path === 'string'
107
+ ? [body.path]
108
+ : 'all'
109
+ const result = await gitStage(cwd, paths)
110
+ return { ok: true, value: result }
111
+ }
120
112
 
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
- }
113
+ if (endpoint === 'unstage') {
114
+ const paths = Array.isArray(body.paths)
115
+ ? (body.paths as string[])
116
+ : body.paths === 'all'
117
+ ? 'all'
118
+ : typeof body.path === 'string'
119
+ ? [body.path]
120
+ : 'all'
121
+ const result = await gitUnstage(cwd, paths)
122
+ return { ok: true, value: result }
123
+ }
132
124
 
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
- }
125
+ if (endpoint === 'discard') {
126
+ const paths = Array.isArray(body.paths)
127
+ ? (body.paths as string[])
128
+ : typeof body.path === 'string'
129
+ ? [body.path]
130
+ : []
131
+ const result = await gitDiscard(cwd, paths)
132
+ return { ok: true, value: result }
133
+ }
144
134
 
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
- }
135
+ if (endpoint === 'commit') {
136
+ const message = typeof body.message === 'string' ? body.message : ''
137
+ const result = await gitCommit(cwd, message)
138
+ return { ok: true, value: result }
139
+ }
154
140
 
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
- }
141
+ if (endpoint === 'fetch') {
142
+ const result = await gitFetch(cwd)
143
+ return { ok: true, value: result }
144
+ }
160
145
 
161
- if (endpoint === 'fetch') {
162
- const result = await gitFetch(cwd)
163
- return { ok: true, value: result }
164
- }
146
+ if (endpoint === 'pull') {
147
+ const result = await gitPull(cwd)
148
+ return { ok: true, value: result }
149
+ }
165
150
 
166
- if (endpoint === 'pull') {
167
- const result = await gitPull(cwd)
168
- return { ok: true, value: result }
169
- }
151
+ if (endpoint === 'push') {
152
+ const result = await gitPush(cwd)
153
+ return { ok: true, value: result }
154
+ }
170
155
 
171
- if (endpoint === 'push') {
172
- const result = await gitPush(cwd)
173
- return { ok: true, value: result }
174
- }
156
+ if (endpoint === 'tags') {
157
+ const tags = await getGitTags(cwd)
158
+ return { ok: true, value: tags }
159
+ }
175
160
 
176
- if (endpoint === 'tags') {
177
- const tags = await getGitTags(cwd)
178
- return { ok: true, value: tags }
179
- }
161
+ if (endpoint === 'createTag') {
162
+ const name = typeof body.name === 'string' ? body.name : ''
163
+ const commit = typeof body.commit === 'string' ? body.commit : undefined
164
+ const message = typeof body.message === 'string' ? body.message : undefined
165
+ const result = await gitCreateTag(cwd, name, commit, message)
166
+ return { ok: true, value: result }
167
+ }
180
168
 
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
- }
169
+ if (endpoint === 'deleteTag') {
170
+ const name = typeof body.name === 'string' ? body.name : ''
171
+ const result = await gitDeleteTag(cwd, name)
172
+ return { ok: true, value: result }
173
+ }
188
174
 
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
- }
175
+ return {
176
+ ok: false,
177
+ error: {
178
+ code: 'git/not_found',
179
+ message: `Unknown git RPC endpoint: ${endpoint}`,
180
+ details: {},
181
+ },
182
+ }
183
+ } catch (error) {
184
+ return {
185
+ ok: false,
186
+ error: {
187
+ code: 'git/error',
188
+ message: error instanceof Error ? error.message : String(error),
189
+ details: {},
190
+ },
191
+ }
192
+ }
193
+ }
194
194
 
195
+ /**
196
+ * Apply the Git host plugin.
197
+ * Sets up direct WebServer HTTP route handlers under `/git/*` and registers the model tool.
198
+ * @param ctx - Context carrying the webServer, sessions, and sandboxPolicy services.
199
+ */
200
+ export function apply(ctx: Context): void {
201
+ const host = ctx as any as HostServices
195
202
 
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
- }
203
+ const resolveTargetDir = (sessionId?: string): string => {
204
+ try {
205
+ if (sessionId) {
206
+ const liveCwd = host.sessions?.get?.(sessionId)?.header?.cwd
207
+ if (liveCwd) return liveCwd
213
208
  }
214
- },
215
- )
209
+ } catch {}
210
+
211
+ try {
212
+ const workspaceRoot = host.sandboxPolicy?.workspaceRoot
213
+ if (workspaceRoot) return workspaceRoot
214
+ } catch {}
215
+
216
+ return process.cwd()
217
+ }
218
+
219
+ // 1. Direct WebServer route registration (Standard, robust DSH plugin route pattern)
220
+ const webServer = (ctx as any).webServer
221
+ if (webServer?.register) {
222
+ ctx.effect(() => webServer.register({
223
+ kind: 'prefix',
224
+ path: '/git',
225
+ handler: async (req: any, res: any) => {
226
+ try {
227
+ if (req.method === 'OPTIONS') {
228
+ res.writeHead(204, {
229
+ 'Access-Control-Allow-Origin': '*',
230
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
231
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
232
+ })
233
+ return res.end()
234
+ }
235
+
236
+ if (req.method !== 'POST') {
237
+ res.writeHead(405, { 'Content-Type': 'application/json' })
238
+ return res.end(JSON.stringify({ ok: false, error: { message: 'Method Not Allowed' } }))
239
+ }
240
+
241
+ const pathname = new URL(req.url ?? '/', 'http://localhost').pathname
242
+ const match = pathname.match(/^\/git\/([a-zA-Z0-9_-]+)/)
243
+ const endpoint = match ? match[1] : undefined
216
244
 
245
+ if (!endpoint) {
246
+ res.writeHead(404, { 'Content-Type': 'application/json' })
247
+ return res.end(JSON.stringify({ ok: false, error: { message: 'Not Found' } }))
248
+ }
249
+
250
+ let rawBody = ''
251
+ req.on('data', (chunk: Buffer) => { rawBody += chunk.toString('utf8') })
252
+ req.on('end', async () => {
253
+ try {
254
+ let body: any = {}
255
+ try {
256
+ if (rawBody.trim()) body = JSON.parse(rawBody)
257
+ } catch {
258
+ res.writeHead(400, { 'Content-Type': 'application/json' })
259
+ return res.end(JSON.stringify({ ok: false, error: { message: 'Invalid JSON body' } }))
260
+ }
261
+
262
+ const rpcId = body?.type === 'client-request' ? body.rpcId : undefined
263
+ const payload = body?.type === 'client-request' ? (body.payload ?? {}) : body
264
+
265
+ const result = await executeGitEndpoint(endpoint, payload, resolveTargetDir)
266
+
267
+ res.writeHead(200, {
268
+ 'Content-Type': 'application/json; charset=utf-8',
269
+ 'Access-Control-Allow-Origin': '*',
270
+ })
271
+
272
+ if (rpcId !== undefined) {
273
+ res.end(JSON.stringify({
274
+ type: 'server-response',
275
+ rpcId,
276
+ result,
277
+ }))
278
+ } else {
279
+ res.end(JSON.stringify(result))
280
+ }
281
+ } catch (err: any) {
282
+ res.writeHead(500, { 'Content-Type': 'application/json' })
283
+ res.end(JSON.stringify({
284
+ ok: false,
285
+ error: { code: 'git/internal_error', message: err?.message ?? String(err) },
286
+ }))
287
+ }
288
+ })
289
+ } catch (err: any) {
290
+ try {
291
+ res.writeHead(500, { 'Content-Type': 'application/json' })
292
+ res.end(JSON.stringify({
293
+ ok: false,
294
+ error: { code: 'git/internal_error', message: err?.message ?? String(err) },
295
+ }))
296
+ } catch {}
297
+ }
298
+ },
299
+ }), 'git-workbench: /git routes')
300
+ }
217
301
 
218
- // Register the agent tool
302
+ // 2. Register the agent tool
219
303
  registerGitTool(ctx, resolveTargetDir)
220
304
  }