@foundation0/git 1.2.3 → 1.2.5
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/README.md +9 -3
- package/mcp/README.md +39 -0
- package/mcp/src/client.ts +10 -1
- package/mcp/src/server.ts +401 -39
- package/package.json +1 -1
- package/src/actions-api.ts +616 -0
- package/src/git-service-api.ts +116 -35
- package/src/index.ts +1 -0
- package/src/platform/gitea-rules.ts +3 -1
package/src/git-service-api.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { getGitPlatformConfig, type GitPlatformConfig } from './platform/config'
|
|
|
3
3
|
import { type GitServiceFlag, type GitServiceFeature, gitServiceFeatureSpec } from './git-service-feature-spec.generated'
|
|
4
4
|
import { type GitApiFeatureMapping } from './platform/gitea-adapter'
|
|
5
5
|
import { attachGitLabelManagementApi } from './label-management'
|
|
6
|
+
import { attachGitActionsApi } from './actions-api'
|
|
6
7
|
import { spawn } from 'node:child_process'
|
|
7
8
|
import crypto from 'node:crypto'
|
|
8
9
|
|
|
@@ -41,7 +42,10 @@ export type GitServiceApi = {
|
|
|
41
42
|
|
|
42
43
|
type ApiCallOptions = {
|
|
43
44
|
method?: string
|
|
44
|
-
|
|
45
|
+
requestBody?: unknown
|
|
46
|
+
requestJson?: unknown
|
|
47
|
+
requestData?: unknown
|
|
48
|
+
requestPayload?: unknown
|
|
45
49
|
json?: unknown
|
|
46
50
|
data?: unknown
|
|
47
51
|
payload?: unknown
|
|
@@ -134,7 +138,18 @@ const mapFlagValues = (
|
|
|
134
138
|
alias.set(canonical, flag.name)
|
|
135
139
|
}
|
|
136
140
|
|
|
137
|
-
const reserved = new Set([
|
|
141
|
+
const reserved = new Set([
|
|
142
|
+
'json',
|
|
143
|
+
'data',
|
|
144
|
+
'payload',
|
|
145
|
+
'headers',
|
|
146
|
+
'query',
|
|
147
|
+
'method',
|
|
148
|
+
'requestbody',
|
|
149
|
+
'requestjson',
|
|
150
|
+
'requestdata',
|
|
151
|
+
'requestpayload',
|
|
152
|
+
])
|
|
138
153
|
|
|
139
154
|
for (const [key, value] of Object.entries(options)) {
|
|
140
155
|
const normalizedKey = normalizeFlagLookup(key)
|
|
@@ -164,30 +179,28 @@ const mapFlagValues = (
|
|
|
164
179
|
const buildRequestBody = (
|
|
165
180
|
method: string,
|
|
166
181
|
options: Record<string, unknown>,
|
|
167
|
-
|
|
182
|
+
defaultBody: Record<string, unknown>,
|
|
168
183
|
): unknown | undefined => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return options.json
|
|
179
|
-
}
|
|
184
|
+
const normalizedMethod = method.toUpperCase()
|
|
185
|
+
const explicit =
|
|
186
|
+
options.requestBody ??
|
|
187
|
+
options.requestJson ??
|
|
188
|
+
options.requestData ??
|
|
189
|
+
options.requestPayload ??
|
|
190
|
+
options.json ??
|
|
191
|
+
options.data ??
|
|
192
|
+
options.payload
|
|
180
193
|
|
|
181
|
-
if (
|
|
182
|
-
return
|
|
194
|
+
if (explicit !== undefined) {
|
|
195
|
+
return explicit
|
|
183
196
|
}
|
|
184
197
|
|
|
185
|
-
if (
|
|
186
|
-
return
|
|
198
|
+
if (!['POST', 'PUT', 'PATCH'].includes(normalizedMethod)) {
|
|
199
|
+
return undefined
|
|
187
200
|
}
|
|
188
201
|
|
|
189
|
-
if (Object.keys(
|
|
190
|
-
return
|
|
202
|
+
if (Object.keys(defaultBody).length > 0) {
|
|
203
|
+
return defaultBody
|
|
191
204
|
}
|
|
192
205
|
|
|
193
206
|
return undefined
|
|
@@ -240,6 +253,48 @@ const canUseAbortSignalTimeout = (): boolean =>
|
|
|
240
253
|
const isTestRuntime = (): boolean =>
|
|
241
254
|
Boolean(process.env.VITEST) || process.env.NODE_ENV === 'test'
|
|
242
255
|
|
|
256
|
+
const toQueryRecord = (raw: Record<string, unknown>): Record<string, string | number | boolean> => {
|
|
257
|
+
const query: Record<string, string | number | boolean> = {}
|
|
258
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
259
|
+
if (typeof value === 'string') {
|
|
260
|
+
query[key] = value
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
263
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
264
|
+
query[key] = value
|
|
265
|
+
continue
|
|
266
|
+
}
|
|
267
|
+
if (typeof value === 'boolean') {
|
|
268
|
+
query[key] = value
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return query
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const buildDefaultWriteBody = (options: Record<string, unknown>): Record<string, unknown> => {
|
|
275
|
+
const reserved = new Set([
|
|
276
|
+
'headers',
|
|
277
|
+
'query',
|
|
278
|
+
'method',
|
|
279
|
+
'requestBody',
|
|
280
|
+
'requestJson',
|
|
281
|
+
'requestData',
|
|
282
|
+
'requestPayload',
|
|
283
|
+
'json',
|
|
284
|
+
'data',
|
|
285
|
+
'payload',
|
|
286
|
+
])
|
|
287
|
+
|
|
288
|
+
const body: Record<string, unknown> = {}
|
|
289
|
+
for (const [key, value] of Object.entries(options)) {
|
|
290
|
+
if (reserved.has(key)) continue
|
|
291
|
+
if (value !== undefined) {
|
|
292
|
+
body[key] = value
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return body
|
|
296
|
+
}
|
|
297
|
+
|
|
243
298
|
const resolveHttpTransport = (requested?: string): 'fetch' | 'curl' => {
|
|
244
299
|
const normalized = (requested ?? '').trim().toLowerCase()
|
|
245
300
|
if (normalized === 'fetch' || normalized === 'curl') {
|
|
@@ -364,17 +419,33 @@ const createMethod = (
|
|
|
364
419
|
): GitServiceApiMethod => {
|
|
365
420
|
return async (...rawArgs: unknown[]) => {
|
|
366
421
|
const { args, options } = splitArgsAndOptions(rawArgs)
|
|
367
|
-
const { flags, unhandled } = mapFlagValues(feature, options)
|
|
368
422
|
const { query: additionalQuery, method: methodOverride, ...bodyOptions } = (options as ApiCallOptions)
|
|
369
423
|
|
|
370
|
-
const
|
|
424
|
+
const baseMapping = await adapter.mapFeature({
|
|
371
425
|
feature,
|
|
372
426
|
args,
|
|
373
|
-
flagValues:
|
|
427
|
+
flagValues: {},
|
|
374
428
|
method: methodOverride,
|
|
375
429
|
})
|
|
430
|
+
const normalizedMethod = baseMapping.method.toUpperCase()
|
|
431
|
+
|
|
432
|
+
const { mapping, extraQuery } = normalizedMethod === 'GET'
|
|
433
|
+
? (() => {
|
|
434
|
+
const { flags, unhandled } = mapFlagValues(feature, options)
|
|
435
|
+
return {
|
|
436
|
+
mapping: adapter.mapFeature({ feature, args, flagValues: flags, method: methodOverride }),
|
|
437
|
+
extraQuery: toQueryRecord(unhandled),
|
|
438
|
+
}
|
|
439
|
+
})()
|
|
440
|
+
: { mapping: Promise.resolve(baseMapping), extraQuery: {} }
|
|
441
|
+
|
|
442
|
+
const resolvedMapping = await mapping
|
|
443
|
+
const mergedQuery = {
|
|
444
|
+
...(additionalQuery ?? {}),
|
|
445
|
+
...(normalizedMethod === 'GET' ? extraQuery : {}),
|
|
446
|
+
}
|
|
376
447
|
|
|
377
|
-
const hydratedPath =
|
|
448
|
+
const hydratedPath = resolvedMapping.mappedPath.map((segment) => {
|
|
378
449
|
if (segment === '{owner}' && defaults.defaultOwner) {
|
|
379
450
|
return defaults.defaultOwner
|
|
380
451
|
}
|
|
@@ -387,9 +458,13 @@ const createMethod = (
|
|
|
387
458
|
})
|
|
388
459
|
assertResolvedMappedPath(hydratedPath, feature.path)
|
|
389
460
|
|
|
390
|
-
const requestBody = buildRequestBody(
|
|
461
|
+
const requestBody = buildRequestBody(
|
|
462
|
+
resolvedMapping.method,
|
|
463
|
+
bodyOptions,
|
|
464
|
+
normalizedMethod === 'GET' ? {} : buildDefaultWriteBody(options),
|
|
465
|
+
)
|
|
391
466
|
const headers = {
|
|
392
|
-
...toHeaderRecord(
|
|
467
|
+
...toHeaderRecord(resolvedMapping.headers),
|
|
393
468
|
...(options.headers ?? {}),
|
|
394
469
|
}
|
|
395
470
|
|
|
@@ -397,15 +472,15 @@ const createMethod = (
|
|
|
397
472
|
headers['Content-Type'] = 'application/json'
|
|
398
473
|
}
|
|
399
474
|
|
|
400
|
-
const requestUrl = buildUrl(
|
|
475
|
+
const requestUrl = buildUrl(resolvedMapping.apiBase, hydratedPath, resolvedMapping.query, mergedQuery)
|
|
401
476
|
const requestInit: RequestInit = {
|
|
402
|
-
method:
|
|
477
|
+
method: resolvedMapping.method,
|
|
403
478
|
headers,
|
|
404
479
|
body: requestBody !== undefined ? JSON.stringify(requestBody) : undefined,
|
|
405
480
|
}
|
|
406
481
|
|
|
407
482
|
const startedAt = Date.now()
|
|
408
|
-
log?.(`http:request ${
|
|
483
|
+
log?.(`http:request ${resolvedMapping.method} ${requestUrl}`)
|
|
409
484
|
try {
|
|
410
485
|
const responseHeaders: Record<string, string> = {}
|
|
411
486
|
let status = 0
|
|
@@ -416,7 +491,7 @@ const createMethod = (
|
|
|
416
491
|
const curlResult = await callCurl(
|
|
417
492
|
requestUrl,
|
|
418
493
|
{
|
|
419
|
-
method:
|
|
494
|
+
method: resolvedMapping.method,
|
|
420
495
|
headers,
|
|
421
496
|
...(requestInit.body !== undefined ? { body: String(requestInit.body) } : {}),
|
|
422
497
|
},
|
|
@@ -465,7 +540,7 @@ const createMethod = (
|
|
|
465
540
|
}
|
|
466
541
|
} catch (error) {
|
|
467
542
|
if ((timeoutSignal && timeoutSignal.aborted) || controller?.signal.aborted) {
|
|
468
|
-
throw new Error(`Request timed out after ${requestTimeoutMs}ms: ${
|
|
543
|
+
throw new Error(`Request timed out after ${requestTimeoutMs}ms: ${resolvedMapping.method} ${requestUrl}`)
|
|
469
544
|
}
|
|
470
545
|
throw error
|
|
471
546
|
} finally {
|
|
@@ -475,17 +550,17 @@ const createMethod = (
|
|
|
475
550
|
}
|
|
476
551
|
}
|
|
477
552
|
|
|
478
|
-
log?.(`http:response ${
|
|
553
|
+
log?.(`http:response ${resolvedMapping.method} ${requestUrl} -> ${status} (${Date.now() - startedAt}ms)`)
|
|
479
554
|
return {
|
|
480
555
|
mapping: {
|
|
481
|
-
...
|
|
556
|
+
...resolvedMapping,
|
|
482
557
|
mappedPath: hydratedPath,
|
|
483
558
|
},
|
|
484
559
|
request: {
|
|
485
560
|
url: requestUrl,
|
|
486
|
-
method:
|
|
561
|
+
method: resolvedMapping.method,
|
|
487
562
|
headers,
|
|
488
|
-
query: [...
|
|
563
|
+
query: [...resolvedMapping.query],
|
|
489
564
|
body: requestBody,
|
|
490
565
|
},
|
|
491
566
|
response: {
|
|
@@ -575,6 +650,12 @@ export const createGitServiceApi = (options: GitServiceApiFactoryOptions = {}):
|
|
|
575
650
|
}
|
|
576
651
|
|
|
577
652
|
attachGitLabelManagementApi(root, defaults)
|
|
653
|
+
attachGitActionsApi(root, {
|
|
654
|
+
config,
|
|
655
|
+
defaults,
|
|
656
|
+
requestTimeoutMs,
|
|
657
|
+
log,
|
|
658
|
+
})
|
|
578
659
|
|
|
579
660
|
return root
|
|
580
661
|
}
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,7 @@ export {
|
|
|
44
44
|
type GitLabelManagementDefaults,
|
|
45
45
|
type GitRepositoryLabel,
|
|
46
46
|
} from './label-management'
|
|
47
|
+
export { attachGitActionsApi, createGitActionsApi, type GitActionsApiContext, type GitActionsApiDefaults, type GitActionsHelpBody } from './actions-api'
|
|
47
48
|
export { resolveProjectRepoIdentity, type GitRepositoryIdentity } from './repository'
|
|
48
49
|
export type {
|
|
49
50
|
GitServiceApi,
|
|
@@ -82,6 +82,9 @@ export const EXACT_GITEA_RULES: GiteaRouteRule[] = [
|
|
|
82
82
|
rule(['workflow', 'run'], 'POST', '/repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches'),
|
|
83
83
|
rule(['workflow', 'view'], 'GET', '/repos/{owner}/{repo}/actions/workflows/{workflow_id}'),
|
|
84
84
|
|
|
85
|
+
// Run commands (Gitea Actions tasks list is the closest analogue)
|
|
86
|
+
rule(['run', 'list'], 'GET', '/repos/{owner}/{repo}/actions/tasks', 'maps run.list to action task list'),
|
|
87
|
+
|
|
85
88
|
// Secret and variable commands
|
|
86
89
|
rule(['secret', 'delete'], 'DELETE', '/repos/{owner}/{repo}/actions/secrets/{secretname}'),
|
|
87
90
|
rule(['secret', 'list'], 'GET', '/repos/{owner}/{repo}/actions/secrets'),
|
|
@@ -112,7 +115,6 @@ export const DEFAULT_GITEA_FIRST_TOKEN_RULES = new Map<string, GiteaRouteRule>([
|
|
|
112
115
|
['pr', rule(['pr'], 'GET', '/repos/{owner}/{repo}/pulls', 'fallback to pull request list')],
|
|
113
116
|
['repo', rule(['repo'], 'GET', '/repos/{owner}/{repo}', 'fallback to repository read')],
|
|
114
117
|
['release', rule(['release'], 'GET', '/repos/{owner}/{repo}/releases', 'fallback to release list')],
|
|
115
|
-
['run', rule(['run'], 'GET', '/repos/{owner}/{repo}/actions/tasks', 'fallback to workflow task list')],
|
|
116
118
|
['workflow', rule(['workflow'], 'GET', '/repos/{owner}/{repo}/actions/workflows', 'fallback to workflow list')],
|
|
117
119
|
['search', rule(['search'], 'GET', '/repos/search', 'fallback to global repo search')],
|
|
118
120
|
['secret', rule(['secret'], 'GET', '/repos/{owner}/{repo}/actions/secrets', 'fallback to repository secret list')],
|