@caozeal/dsh-next-git 0.0.0-stage → 0.1.3

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/index.js ADDED
@@ -0,0 +1,171 @@
1
+ /**
2
+ * git-manager — Host 入口。
3
+ *
4
+ * 注册认证路由 `POST /api/git-manager`(由 connection 层做浏览器凭据校验),
5
+ * 把请求分发给 src/operations.js;git 进程统一走 ctx.subprocess,AI 提交信息
6
+ * 走可选的 llm + agentDefaultModel 服务(缺失时降级)。
7
+ */
8
+ import { cleanGitEnv, GitError } from './src/git.js'
9
+ import { createOperations } from './src/operations.js'
10
+
11
+ export const name = 'git-manager'
12
+
13
+ /** connection 提供认证 fetch 路由;subprocess 执行 git。 */
14
+ export const inject = ['connection', 'subprocess']
15
+
16
+ const ROUTE_PATH = '/api/git-manager'
17
+ const STDOUT_MAX_BYTES = 8 * 1024 * 1024
18
+ const STDERR_MAX_BYTES = 1024 * 1024
19
+
20
+ /** 构造 JSON Response。 */
21
+ function jsonResponse(status, payload) {
22
+ return new Response(JSON.stringify(payload), {
23
+ status,
24
+ headers: {
25
+ 'content-type': 'application/json; charset=utf-8',
26
+ 'cache-control': 'no-store',
27
+ },
28
+ })
29
+ }
30
+
31
+ function errorPayload(error) {
32
+ const code = error instanceof GitError ? error.code : 'internal'
33
+ const raw = error && error.message ? String(error.message) : String(error)
34
+ const message = raw.length > 4000 ? `${raw.slice(0, 4000)}…` : raw
35
+ return { ok: false, error: { code, message } }
36
+ }
37
+
38
+ /** 读取 subprocess 收集模式下的输出。 */
39
+ function readCollected(handle, which) {
40
+ try {
41
+ const reader = handle && handle.collected ? handle.collected[which] : undefined
42
+ if (!reader) return ''
43
+ return reader.readFrom(0).text ?? ''
44
+ } catch {
45
+ return ''
46
+ }
47
+ }
48
+
49
+ /** 把 ctx.subprocess 包装成 operations 需要的 exec。 */
50
+ function createExec(ctx) {
51
+ return async function exec(argv, options = {}) {
52
+ const { cwd, signal, timeoutMs = 30000 } = options
53
+ const controller = new AbortController()
54
+ let timedOut = false
55
+ const onAbort = () => controller.abort()
56
+ if (signal) {
57
+ if (signal.aborted) controller.abort()
58
+ else signal.addEventListener('abort', onAbort, { once: true })
59
+ }
60
+ const timer = setTimeout(() => {
61
+ timedOut = true
62
+ controller.abort()
63
+ }, Math.max(100, timeoutMs))
64
+ try {
65
+ const handle = ctx.subprocess.spawn({
66
+ argv,
67
+ cwd,
68
+ stdio: {
69
+ stdin: 'ignore',
70
+ stdout: { maxBytes: STDOUT_MAX_BYTES },
71
+ stderr: { maxBytes: STDERR_MAX_BYTES },
72
+ },
73
+ graceMs: 3000,
74
+ signal: controller.signal,
75
+ env: cleanGitEnv(),
76
+ })
77
+ const outcome = await handle.done
78
+ const stdout = readCollected(handle, 'stdout')
79
+ const stderr = readCollected(handle, 'stderr')
80
+ if (timedOut || controller.signal.aborted) {
81
+ throw new GitError(`git 命令超时或被取消:${argv.join(' ')}`, {
82
+ code: 'timeout',
83
+ stdout,
84
+ stderr,
85
+ })
86
+ }
87
+ return { code: outcome.exitCode, stdout, stderr }
88
+ } catch (error) {
89
+ if (error instanceof GitError) throw error
90
+ if (timedOut || controller.signal.aborted) {
91
+ throw new GitError(`git 命令超时或被取消:${argv.join(' ')}`, { code: 'timeout' })
92
+ }
93
+ const message = error && error.message ? error.message : String(error)
94
+ const code = error && (error.code === 'ENOENT' || /\bENOENT\b/.test(message))
95
+ ? 'cwd-unavailable'
96
+ : 'internal'
97
+ throw new GitError(
98
+ code === 'cwd-unavailable'
99
+ ? `无法访问工作目录或 git 可执行文件(${cwd}):${message}`
100
+ : `无法执行 ${argv.join(' ')}:${message}`,
101
+ { code },
102
+ )
103
+ } finally {
104
+ clearTimeout(timer)
105
+ if (signal) signal.removeEventListener('abort', onAbort)
106
+ }
107
+ }
108
+ }
109
+
110
+ /** 读取默认模型选择;任何异常都视为未配置。 */
111
+ function readSelection(ctx) {
112
+ try {
113
+ const service = ctx.get('agentDefaultModel')
114
+ if (!service || typeof service.currentSelection !== 'function') return null
115
+ return service.currentSelection() ?? null
116
+ } catch {
117
+ return null
118
+ }
119
+ }
120
+
121
+ /** 处理一次 HTTP 请求。 */
122
+ async function handleRequest(request, operations) {
123
+ if (request.method !== 'POST') {
124
+ return jsonResponse(405, { ok: false, error: { code: 'invalid-params', message: '只支持 POST' } })
125
+ }
126
+ let body
127
+ try {
128
+ body = await request.json()
129
+ } catch {
130
+ return jsonResponse(400, { ok: false, error: { code: 'invalid-params', message: '请求体不是合法 JSON' } })
131
+ }
132
+ if (body === null || typeof body !== 'object' || Array.isArray(body)) {
133
+ return jsonResponse(400, { ok: false, error: { code: 'invalid-params', message: '请求体必须是 JSON 对象' } })
134
+ }
135
+ const { op, ...params } = body
136
+ try {
137
+ const data = await operations.handle(op, params, request.signal)
138
+ return jsonResponse(200, { ok: true, data })
139
+ } catch (error) {
140
+ // 参数错误(含未知 op)用 400;git 业务失败仍是 200 + ok:false,
141
+ // 让 UI 统一按 body 处理而不把错误码当网络故障。
142
+ const status = error instanceof GitError && error.code === 'invalid-params' ? 400 : 200
143
+ return jsonResponse(status, errorPayload(error))
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Host 插件主体。
149
+ * @param {import('@deepseek-ai/cordis').Context} ctx
150
+ */
151
+ export function apply(ctx) {
152
+ const operations = createOperations({
153
+ exec: createExec(ctx),
154
+ ai: () => ({
155
+ llm: ctx.get('llm'),
156
+ selection: readSelection(ctx),
157
+ }),
158
+ })
159
+ ctx.effect(() => {
160
+ const dispose = ctx.connection.fetch.register({
161
+ path: ROUTE_PATH,
162
+ methods: ['POST'],
163
+ requestBody: 'buffered',
164
+ fetch: (request) => handleRequest(request, operations),
165
+ })
166
+ return () => {
167
+ void dispose()
168
+ }
169
+ }, 'git-manager: /api/git-manager route')
170
+ console.log(`[git-manager] 已注册 ${ROUTE_PATH}`)
171
+ }