ya-git-jira 1.6.0 → 2.0.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/.opencode/skills/architecture/SKILL.md +45 -0
- package/.opencode/skills/code-style/SKILL.md +76 -0
- package/.opencode/skills/git-confluence/SKILL.md +82 -0
- package/.opencode/skills/git-jira/SKILL.md +63 -0
- package/.opencode/skills/git-lab/SKILL.md +102 -0
- package/AGENTS.md +50 -0
- package/README.md +106 -106
- package/bin/git-api.ts +70 -0
- package/bin/git-confluence-page-search.ts +58 -0
- package/bin/git-confluence-page-show.ts +61 -0
- package/bin/git-confluence-page-update.ts +77 -0
- package/bin/git-confluence-page.ts +28 -0
- package/bin/git-confluence-space-list.ts +34 -0
- package/bin/git-confluence-space.ts +24 -0
- package/bin/git-confluence-whoami.ts +33 -0
- package/bin/git-confluence.ts +27 -0
- package/bin/git-jira-start.ts +1 -1
- package/bin/git-jira-whoami.ts +32 -0
- package/bin/git-jira.ts +2 -0
- package/bin/git-lab-project-mr-list.ts +57 -0
- package/bin/git-lab-project-mr.ts +24 -0
- package/bin/git-lab-project-pipeline-jobs.ts +46 -0
- package/bin/git-lab-project-pipeline-latest.ts +47 -0
- package/bin/git-lab-project-pipeline-log.ts +49 -0
- package/bin/git-lab-project-pipeline.ts +6 -0
- package/bin/git-lab-project.ts +5 -1
- package/bin/gitj-install-skills.ts +126 -0
- package/bin/gitj.ts +12 -0
- package/dist/bin/git-api.js +2156 -0
- package/dist/bin/git-bump.js +132 -121
- package/dist/bin/git-confluence-page-search.js +2079 -0
- package/dist/bin/git-confluence-page-show.js +2082 -0
- package/dist/bin/git-confluence-page-update.js +2093 -0
- package/dist/bin/git-confluence-page.js +2186 -0
- package/dist/bin/git-confluence-space-list.js +2061 -0
- package/dist/bin/git-confluence-space.js +2073 -0
- package/dist/bin/git-confluence-whoami.js +2060 -0
- package/dist/bin/git-confluence.js +2251 -0
- package/dist/bin/git-jira-issue-list.js +136 -125
- package/dist/bin/git-jira-issue-show.js +136 -125
- package/dist/bin/git-jira-issue.js +140 -129
- package/dist/bin/git-jira-start.js +138 -127
- package/dist/bin/git-jira-whoami.js +1972 -0
- package/dist/bin/git-jira.js +170 -139
- package/dist/bin/git-lab-group-list.js +321 -279
- package/dist/bin/git-lab-group.js +323 -281
- package/dist/bin/git-lab-merge-active.js +321 -279
- package/dist/bin/git-lab-merge-todo.js +321 -279
- package/dist/bin/git-lab-merge-train-list.js +289 -273
- package/dist/bin/git-lab-merge-train.js +291 -275
- package/dist/bin/git-lab-merge.js +330 -288
- package/dist/bin/git-lab-namespace-list.js +138 -127
- package/dist/bin/git-lab-namespace.js +140 -129
- package/dist/bin/git-lab-project-list.js +288 -272
- package/dist/bin/git-lab-project-mr-list.js +2740 -0
- package/dist/bin/git-lab-project-mr.js +2752 -0
- package/dist/bin/git-lab-project-pipeline-jobs.js +2734 -0
- package/dist/bin/git-lab-project-pipeline-latest.js +2736 -0
- package/dist/bin/git-lab-project-pipeline-list.js +323 -281
- package/dist/bin/git-lab-project-pipeline-log.js +2739 -0
- package/dist/bin/git-lab-project-pipeline.js +437 -292
- package/dist/bin/git-lab-project-whereami.js +292 -276
- package/dist/bin/git-lab-project.js +563 -288
- package/dist/bin/git-lab-whoami.js +142 -131
- package/dist/bin/git-lab.js +575 -338
- package/dist/bin/gitj-install-skills.js +1954 -0
- package/dist/bin/gitj.js +1385 -473
- package/dist/index.js +371 -187
- package/index.ts +1 -0
- package/lib/api.ts +177 -0
- package/lib/confluence/api.ts +132 -0
- package/lib/confluence/config.ts +25 -0
- package/lib/confluence/index.ts +3 -0
- package/lib/confluence/types.ts +59 -0
- package/lib/gitlab/index.ts +1 -0
- package/lib/gitlab/job.ts +31 -0
- package/lib/gitlab/merge-request.ts +20 -0
- package/lib/gitlab/pipeline.ts +28 -1
- package/lib/gitlab/project.ts +14 -5
- package/lib/help.ts +40 -0
- package/lib/jira.ts +2 -2
- package/package.json +18 -2
- package/tests/all-help.test.ts +6 -1
- package/tests/gitj.test.ts +1 -1
- package/tests/help-all.test.ts +29 -0
- package/bun.lockb +0 -0
package/index.ts
CHANGED
package/lib/api.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { JSONValue } from './json'
|
|
2
|
+
import { getGitlabConfig } from './gitlab/config'
|
|
3
|
+
import { getJiraConfig } from './jira'
|
|
4
|
+
import { getConfluenceConfig } from './confluence/config'
|
|
5
|
+
|
|
6
|
+
export interface ApiResponse {
|
|
7
|
+
status: number
|
|
8
|
+
headers: Headers
|
|
9
|
+
body: JSONValue
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ServiceDef {
|
|
13
|
+
getConfig: () => Promise<{ host: string; token: string }>
|
|
14
|
+
basePath: string
|
|
15
|
+
authHeader: (token: string) => [string, string]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const services: Record<string, ServiceDef> = {
|
|
19
|
+
gitlab: {
|
|
20
|
+
getConfig: getGitlabConfig,
|
|
21
|
+
basePath: '/api/v4',
|
|
22
|
+
authHeader: (token) => ['Private-Token', token],
|
|
23
|
+
},
|
|
24
|
+
jira: {
|
|
25
|
+
getConfig: getJiraConfig,
|
|
26
|
+
basePath: '/rest/api/3',
|
|
27
|
+
authHeader: (token) => ['Authorization', `Basic ${token}`],
|
|
28
|
+
},
|
|
29
|
+
confluence: {
|
|
30
|
+
getConfig: getConfluenceConfig,
|
|
31
|
+
basePath: '/wiki/api/v2',
|
|
32
|
+
authHeader: (token) => ['Authorization', `Basic ${token}`],
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const serviceNames = Object.keys(services)
|
|
37
|
+
|
|
38
|
+
export function getService(name: string): ServiceDef {
|
|
39
|
+
const svc = services[name]
|
|
40
|
+
if (!svc) {
|
|
41
|
+
throw new Error(`Unknown service: ${name}. Expected one of: ${serviceNames.join(', ')}`)
|
|
42
|
+
}
|
|
43
|
+
return svc
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RequestOptions {
|
|
47
|
+
raw?: boolean
|
|
48
|
+
data?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function apiRequest(
|
|
52
|
+
serviceName: string,
|
|
53
|
+
method: string,
|
|
54
|
+
endpoint: string,
|
|
55
|
+
options?: RequestOptions,
|
|
56
|
+
): Promise<ApiResponse> {
|
|
57
|
+
const svc = getService(serviceName)
|
|
58
|
+
const { host, token } = await svc.getConfig()
|
|
59
|
+
const [headerName, headerValue] = svc.authHeader(token)
|
|
60
|
+
|
|
61
|
+
let path = endpoint
|
|
62
|
+
if (path.startsWith('/')) {
|
|
63
|
+
path = path.slice(1)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let url: string
|
|
67
|
+
if (options?.raw) {
|
|
68
|
+
url = `https://${host}/${path}`
|
|
69
|
+
} else {
|
|
70
|
+
url = `https://${host}${svc.basePath}/${path}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const headers = new Headers()
|
|
74
|
+
headers.append(headerName, headerValue)
|
|
75
|
+
headers.append('Accept', 'application/json')
|
|
76
|
+
|
|
77
|
+
const fetchOptions: RequestInit = { method, headers }
|
|
78
|
+
if (options?.data) {
|
|
79
|
+
headers.append('Content-Type', 'application/json')
|
|
80
|
+
fetchOptions.body = options.data
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const request = new Request(url, fetchOptions)
|
|
84
|
+
const response = await fetch(request)
|
|
85
|
+
const body = await response.json() as JSONValue
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
status: response.status,
|
|
89
|
+
headers: response.headers,
|
|
90
|
+
body,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getNextLink(link: string | null): string | undefined {
|
|
95
|
+
if (!link) {
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
const regex = /<([^>]+)>; rel="next"/
|
|
99
|
+
const match = link.match(regex)
|
|
100
|
+
return match ? match[1] : undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function apiPaginate(
|
|
104
|
+
serviceName: string,
|
|
105
|
+
endpoint: string,
|
|
106
|
+
options?: RequestOptions,
|
|
107
|
+
): Promise<ApiResponse> {
|
|
108
|
+
const svc = getService(serviceName)
|
|
109
|
+
const { host, token } = await svc.getConfig()
|
|
110
|
+
const [headerName, headerValue] = svc.authHeader(token)
|
|
111
|
+
|
|
112
|
+
let path = endpoint
|
|
113
|
+
if (path.startsWith('/')) {
|
|
114
|
+
path = path.slice(1)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let url: string
|
|
118
|
+
if (options?.raw) {
|
|
119
|
+
url = `https://${host}/${path}`
|
|
120
|
+
} else {
|
|
121
|
+
url = `https://${host}${svc.basePath}/${path}`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const headers = new Headers()
|
|
125
|
+
headers.append(headerName, headerValue)
|
|
126
|
+
headers.append('Accept', 'application/json')
|
|
127
|
+
const fetchOptions = { method: 'GET', headers }
|
|
128
|
+
|
|
129
|
+
const origin = `https://${host}`
|
|
130
|
+
let allResults: Array<JSONValue> = []
|
|
131
|
+
let lastStatus = 0
|
|
132
|
+
let lastHeaders: Headers = new Headers()
|
|
133
|
+
|
|
134
|
+
while (url) {
|
|
135
|
+
const request = new Request(url, fetchOptions)
|
|
136
|
+
const response = await fetch(request)
|
|
137
|
+
lastStatus = response.status
|
|
138
|
+
lastHeaders = response.headers
|
|
139
|
+
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
const body = await response.json() as JSONValue
|
|
142
|
+
return { status: response.status, headers: response.headers, body }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const body = await response.json() as JSONValue
|
|
146
|
+
|
|
147
|
+
// handle array responses (gitlab style)
|
|
148
|
+
if (Array.isArray(body)) {
|
|
149
|
+
allResults = allResults.concat(body)
|
|
150
|
+
// handle envelope responses with .results (confluence style)
|
|
151
|
+
} else if (body && typeof body === 'object' && 'results' in body) {
|
|
152
|
+
const envelope = body as { results?: Array<JSONValue>; _links?: { next?: string } }
|
|
153
|
+
if (Array.isArray(envelope.results)) {
|
|
154
|
+
allResults = allResults.concat(envelope.results)
|
|
155
|
+
}
|
|
156
|
+
// confluence v1 pagination via _links.next
|
|
157
|
+
const linksNext = envelope._links?.next
|
|
158
|
+
if (linksNext) {
|
|
159
|
+
url = linksNext.startsWith('/') ? `${origin}${linksNext}` : linksNext
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
// single object response -- no pagination possible
|
|
164
|
+
return { status: response.status, headers: response.headers, body }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// standard Link header pagination
|
|
168
|
+
const nextLink = getNextLink(response.headers.get('Link'))
|
|
169
|
+
if (nextLink) {
|
|
170
|
+
url = nextLink.startsWith('/') ? `${origin}${nextLink}` : nextLink
|
|
171
|
+
} else {
|
|
172
|
+
url = ''
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { status: lastStatus, headers: lastHeaders, body: allResults }
|
|
177
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { JSONValue } from '../json'
|
|
2
|
+
import { getConfluenceConfig } from './config'
|
|
3
|
+
|
|
4
|
+
function getNextLink(link: string | null): string | undefined {
|
|
5
|
+
if (!link) {
|
|
6
|
+
return undefined
|
|
7
|
+
}
|
|
8
|
+
const regex = /<([^>]+)>; rel="next"/
|
|
9
|
+
const match = link.match(regex)
|
|
10
|
+
const next = match ? match[1] : undefined
|
|
11
|
+
return next
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function confluenceApi(endpoint: string): Promise<JSONValue> {
|
|
15
|
+
if (endpoint.startsWith('/')) {
|
|
16
|
+
console.warn(`confluenceApi: endpoint ${endpoint} starts with /, removing it`)
|
|
17
|
+
endpoint = endpoint.slice(1)
|
|
18
|
+
}
|
|
19
|
+
const method = 'GET'
|
|
20
|
+
const { host, token } = await getConfluenceConfig()
|
|
21
|
+
const base = `https://${host}/wiki/api/v2`
|
|
22
|
+
const uri = `${base}/${endpoint}`
|
|
23
|
+
const auth = `Basic ${token}`
|
|
24
|
+
const headers = new Headers()
|
|
25
|
+
headers.append('Authorization', auth)
|
|
26
|
+
headers.append('Accept', 'application/json')
|
|
27
|
+
const options = {
|
|
28
|
+
method,
|
|
29
|
+
headers,
|
|
30
|
+
}
|
|
31
|
+
let request = new Request(uri, options)
|
|
32
|
+
const response = await fetch(request)
|
|
33
|
+
let link = getNextLink(response.headers.get('Link'))
|
|
34
|
+
const body = await response.json() as JSONValue & { results?: Array<JSONValue> }
|
|
35
|
+
if (!body.results) {
|
|
36
|
+
return body
|
|
37
|
+
}
|
|
38
|
+
let result: Array<JSONValue> = body.results
|
|
39
|
+
const origin = `https://${host}`
|
|
40
|
+
while (link) {
|
|
41
|
+
const url = link.startsWith('/') ? `${origin}${link}` : link
|
|
42
|
+
let request = new Request(url, options)
|
|
43
|
+
const next_response = await fetch(request)
|
|
44
|
+
link = getNextLink(next_response.headers.get('Link'))
|
|
45
|
+
const next_body = await next_response.json() as JSONValue & { results?: Array<JSONValue> }
|
|
46
|
+
if (next_body.results) {
|
|
47
|
+
result = result.concat(next_body.results)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function confluenceApiWrite(endpoint: string, method: string, body: JSONValue): Promise<JSONValue> {
|
|
54
|
+
if (endpoint.startsWith('/')) {
|
|
55
|
+
console.warn(`confluenceApiWrite: endpoint ${endpoint} starts with /, removing it`)
|
|
56
|
+
endpoint = endpoint.slice(1)
|
|
57
|
+
}
|
|
58
|
+
const { host, token } = await getConfluenceConfig()
|
|
59
|
+
const base = `https://${host}/wiki/api/v2`
|
|
60
|
+
const uri = `${base}/${endpoint}`
|
|
61
|
+
const auth = `Basic ${token}`
|
|
62
|
+
const headers = new Headers()
|
|
63
|
+
headers.append('Authorization', auth)
|
|
64
|
+
headers.append('Accept', 'application/json')
|
|
65
|
+
headers.append('Content-Type', 'application/json')
|
|
66
|
+
const options = {
|
|
67
|
+
method,
|
|
68
|
+
headers,
|
|
69
|
+
body: JSON.stringify(body),
|
|
70
|
+
}
|
|
71
|
+
const request = new Request(uri, options)
|
|
72
|
+
const response = await fetch(request)
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
const text = await response.text()
|
|
75
|
+
throw new Error(`Confluence API ${method} ${endpoint} failed (${response.status}): ${text}`)
|
|
76
|
+
}
|
|
77
|
+
const result = await response.json()
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function confluenceSearch(cql: string): Promise<JSONValue> {
|
|
82
|
+
const { host, token } = await getConfluenceConfig()
|
|
83
|
+
const base = `https://${host}/wiki/rest/api`
|
|
84
|
+
const auth = `Basic ${token}`
|
|
85
|
+
const headers = new Headers()
|
|
86
|
+
headers.append('Authorization', auth)
|
|
87
|
+
headers.append('Accept', 'application/json')
|
|
88
|
+
const options = { method: 'GET', headers }
|
|
89
|
+
const origin = `https://${host}`
|
|
90
|
+
|
|
91
|
+
let uri = `${base}/search?cql=${encodeURIComponent(cql)}&limit=25`
|
|
92
|
+
let allResults: Array<JSONValue> = []
|
|
93
|
+
|
|
94
|
+
while (uri) {
|
|
95
|
+
const request = new Request(uri, options)
|
|
96
|
+
const response = await fetch(request)
|
|
97
|
+
const body = await response.json() as JSONValue & {
|
|
98
|
+
results?: Array<JSONValue>
|
|
99
|
+
_links?: { next?: string }
|
|
100
|
+
}
|
|
101
|
+
if (body.results) {
|
|
102
|
+
allResults = allResults.concat(body.results)
|
|
103
|
+
}
|
|
104
|
+
const next = body._links?.next
|
|
105
|
+
uri = next ? (next.startsWith('/') ? `${origin}${next}` : next) : ''
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return allResults
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function confluenceApiV1(endpoint: string): Promise<JSONValue> {
|
|
112
|
+
if (endpoint.startsWith('/')) {
|
|
113
|
+
console.warn(`confluenceApiV1: endpoint ${endpoint} starts with /, removing it`)
|
|
114
|
+
endpoint = endpoint.slice(1)
|
|
115
|
+
}
|
|
116
|
+
const method = 'GET'
|
|
117
|
+
const { host, token } = await getConfluenceConfig()
|
|
118
|
+
const base = `https://${host}/wiki/rest/api`
|
|
119
|
+
const uri = `${base}/${endpoint}`
|
|
120
|
+
const auth = `Basic ${token}`
|
|
121
|
+
const headers = new Headers()
|
|
122
|
+
headers.append('Authorization', auth)
|
|
123
|
+
headers.append('Accept', 'application/json')
|
|
124
|
+
const options = {
|
|
125
|
+
method,
|
|
126
|
+
headers,
|
|
127
|
+
}
|
|
128
|
+
const request = new Request(uri, options)
|
|
129
|
+
const response = await fetch(request)
|
|
130
|
+
const result = await response.json()
|
|
131
|
+
return result
|
|
132
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getConfig } from '../git'
|
|
2
|
+
|
|
3
|
+
export interface ConfluenceConfig {
|
|
4
|
+
host: string
|
|
5
|
+
token: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const gitEmailP = getConfig('user.email')
|
|
9
|
+
const jiraEmailP = getConfig('jira.user')
|
|
10
|
+
const confluenceEmailP = getConfig('confluence.user', { expectQuiet: true })
|
|
11
|
+
const jiraHostP = getConfig('jira.host')
|
|
12
|
+
const confluenceHostP = getConfig('confluence.host', { expectQuiet: true })
|
|
13
|
+
const jiraTokenP = getConfig('jira.token')
|
|
14
|
+
const confluenceTokenP = getConfig('confluence.token', { expectQuiet: true })
|
|
15
|
+
|
|
16
|
+
export async function getConfluenceConfig(): Promise<ConfluenceConfig> {
|
|
17
|
+
const host = await confluenceHostP || await jiraHostP
|
|
18
|
+
if (!host) throw new Error('confluence.host or jira.host not in git config')
|
|
19
|
+
const user = await confluenceEmailP || await jiraEmailP || await gitEmailP
|
|
20
|
+
if (!user) throw new Error('confluence.user, jira.user, or user.email not in git config')
|
|
21
|
+
const pat = await confluenceTokenP || await jiraTokenP
|
|
22
|
+
if (!pat) throw new Error('confluence.token or jira.token not in git config')
|
|
23
|
+
const token = Buffer.from(`${user}:${pat}`).toString('base64')
|
|
24
|
+
return { host, token }
|
|
25
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { JSONValue } from '../json'
|
|
2
|
+
|
|
3
|
+
export type ConfluenceUser = JSONValue & {
|
|
4
|
+
type: string
|
|
5
|
+
accountId: string
|
|
6
|
+
accountType: string
|
|
7
|
+
email: string
|
|
8
|
+
publicName: string
|
|
9
|
+
displayName: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type Space = JSONValue & {
|
|
13
|
+
id: string
|
|
14
|
+
key: string
|
|
15
|
+
name: string
|
|
16
|
+
type: string
|
|
17
|
+
status: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type SearchResult = JSONValue & {
|
|
21
|
+
content: {
|
|
22
|
+
id: string
|
|
23
|
+
type: string
|
|
24
|
+
status: string
|
|
25
|
+
title: string
|
|
26
|
+
}
|
|
27
|
+
title: string
|
|
28
|
+
excerpt: string
|
|
29
|
+
url: string
|
|
30
|
+
resultGlobalContainer: {
|
|
31
|
+
title: string
|
|
32
|
+
displayUrl: string
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type Page = JSONValue & {
|
|
37
|
+
id: string
|
|
38
|
+
status: string
|
|
39
|
+
title: string
|
|
40
|
+
spaceId: string
|
|
41
|
+
parentId: string
|
|
42
|
+
parentType: string
|
|
43
|
+
position: number
|
|
44
|
+
version: {
|
|
45
|
+
number: number
|
|
46
|
+
message: string
|
|
47
|
+
createdAt: string
|
|
48
|
+
}
|
|
49
|
+
body?: {
|
|
50
|
+
storage?: {
|
|
51
|
+
representation: string
|
|
52
|
+
value: string
|
|
53
|
+
}
|
|
54
|
+
atlas_doc_format?: {
|
|
55
|
+
representation: string
|
|
56
|
+
value: string
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
package/lib/gitlab/index.ts
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { dlog } from "./dlog"
|
|
2
|
+
import { projectScopedGet, projectScopedGetText } from "./project"
|
|
3
|
+
import type { JSONValue } from "../json"
|
|
4
|
+
|
|
5
|
+
export type Job = JSONValue & {
|
|
6
|
+
id: number
|
|
7
|
+
name: string
|
|
8
|
+
status: string
|
|
9
|
+
stage: string
|
|
10
|
+
ref: string
|
|
11
|
+
web_url: string
|
|
12
|
+
duration: number | null
|
|
13
|
+
finished_at: string | null
|
|
14
|
+
failure_reason: string | null
|
|
15
|
+
pipeline: { id: number }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function getPipelineJobs(pipelineId: number): Promise<Array<Job>> {
|
|
19
|
+
dlog(`getPipelineJobs pipelineId: ${pipelineId}`)
|
|
20
|
+
return await projectScopedGet(`pipelines/${pipelineId}/jobs`) as Array<Job>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function getJob(jobId: number): Promise<Job> {
|
|
24
|
+
dlog(`getJob jobId: ${jobId}`)
|
|
25
|
+
return await projectScopedGet(`jobs/${jobId}`) as Job
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getJobLog(jobId: number): Promise<string> {
|
|
29
|
+
dlog(`getJobLog jobId: ${jobId}`)
|
|
30
|
+
return await projectScopedGetText(`jobs/${jobId}/trace`)
|
|
31
|
+
}
|
|
@@ -5,13 +5,17 @@ import { gitlabApi } from './api'
|
|
|
5
5
|
|
|
6
6
|
export type MergeRequest = JSONValue & {
|
|
7
7
|
id: number
|
|
8
|
+
iid: number
|
|
8
9
|
title : string
|
|
9
10
|
description : string
|
|
10
11
|
state : string
|
|
12
|
+
draft: boolean
|
|
11
13
|
source_branch: string
|
|
12
14
|
target_branch: string
|
|
13
15
|
web_url: string
|
|
14
16
|
merge_status: string
|
|
17
|
+
author: { username: string }
|
|
18
|
+
labels: string[]
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
export async function getMergeRequest(id: string): Promise<MergeRequest> {
|
|
@@ -29,3 +33,19 @@ export async function getMyMergeRequestsToReview() : Promise<Array<MergeRequest>
|
|
|
29
33
|
const me = await whoami()
|
|
30
34
|
return await gitlabApi(`merge_requests?state=opened&reviewer_id=${me.id}`) as Array<MergeRequest>
|
|
31
35
|
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* List open merge requests for a specific project and source branch.
|
|
39
|
+
* @param projectPath - Full project path (e.g. "etagen-internal/eta-lib/base")
|
|
40
|
+
* @param sourceBranch - Source branch name to filter on
|
|
41
|
+
*/
|
|
42
|
+
export async function getMergeRequestsByBranch(
|
|
43
|
+
projectPath: string,
|
|
44
|
+
sourceBranch: string,
|
|
45
|
+
): Promise<Array<MergeRequest>> {
|
|
46
|
+
const project = encodeURIComponent(projectPath)
|
|
47
|
+
const branch = encodeURIComponent(sourceBranch)
|
|
48
|
+
return await gitlabApi(
|
|
49
|
+
`projects/${project}/merge_requests?state=opened&source_branch=${branch}`
|
|
50
|
+
) as Array<MergeRequest>
|
|
51
|
+
}
|
package/lib/gitlab/pipeline.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { dlog } from "./dlog"
|
|
2
2
|
import { projectScopedGet } from "./project"
|
|
3
3
|
import { whoami } from "./user"
|
|
4
|
+
import { gitlabApi } from "./api"
|
|
5
|
+
import { getCurrentBranch } from "../git"
|
|
4
6
|
import type { JSONValue } from "../json"
|
|
5
7
|
|
|
6
8
|
export type Pipeline = JSONValue & {
|
|
@@ -12,7 +14,7 @@ export type Pipeline = JSONValue & {
|
|
|
12
14
|
updated_at: string // datetime string like "2021-03-18T15:00:00.000Z"
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
export type PipelineStatus = 'success' | 'running'
|
|
17
|
+
export type PipelineStatus = 'success' | 'running' | 'failed' | 'canceled' | 'pending' | 'created'
|
|
16
18
|
|
|
17
19
|
export interface GetPipelineOptions {
|
|
18
20
|
days: number
|
|
@@ -30,3 +32,28 @@ export async function getProjectPipelines(options: GetPipelineOptions): Promise<
|
|
|
30
32
|
dlog(`updated: ${updated}`)
|
|
31
33
|
return await projectScopedGet(`pipelines?status=${status}&username=${username}&updated_after=${updated}`) as Array<Pipeline>
|
|
32
34
|
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get pipelines for a specific merge request.
|
|
38
|
+
* Returns pipelines sorted by ID descending (most recent first).
|
|
39
|
+
* @param projectPath - Full project path (e.g. "etagen-internal/eta-lib/base")
|
|
40
|
+
* @param mrIid - The merge request IID (project-scoped ID)
|
|
41
|
+
*/
|
|
42
|
+
export async function getMergeRequestPipelines(
|
|
43
|
+
projectPath: string,
|
|
44
|
+
mrIid: number,
|
|
45
|
+
): Promise<Array<Pipeline>> {
|
|
46
|
+
const project = encodeURIComponent(projectPath)
|
|
47
|
+
return await gitlabApi(
|
|
48
|
+
`projects/${project}/merge_requests/${mrIid}/pipelines`
|
|
49
|
+
) as Array<Pipeline>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function getLatestPipeline(): Promise<Pipeline | undefined> {
|
|
53
|
+
const ref = await getCurrentBranch()
|
|
54
|
+
dlog(`getLatestPipeline ref: ${ref}`)
|
|
55
|
+
const pipelines = await projectScopedGet(
|
|
56
|
+
`pipelines?ref=${encodeURIComponent(ref)}&per_page=1&order_by=id&sort=desc`
|
|
57
|
+
) as Array<Pipeline>
|
|
58
|
+
return pipelines.length > 0 ? pipelines[0] : undefined
|
|
59
|
+
}
|
package/lib/gitlab/project.ts
CHANGED
|
@@ -50,7 +50,7 @@ export async function findProject(ssh_url: string): Promise<Project | undefined>
|
|
|
50
50
|
return project
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
async function projectScopedRequest(endpoint: string): Promise<Response> {
|
|
54
54
|
if (endpoint.startsWith("/")) {
|
|
55
55
|
console.warn(`gitlabApi: endpoint ${endpoint} starts with /, removing it`)
|
|
56
56
|
endpoint = endpoint.slice(1)
|
|
@@ -63,16 +63,25 @@ export async function projectScopedGet(endpoint: string): Promise<JSONValue> {
|
|
|
63
63
|
throw new Error(`Could not find project for remote ${remote}`)
|
|
64
64
|
}
|
|
65
65
|
const base = `https://${host}/api/v4/projects/${project.id}`
|
|
66
|
-
const
|
|
67
|
-
|
|
66
|
+
const sep = endpoint.includes('?') ? '&' : '?'
|
|
67
|
+
const uri = `${base}/${endpoint}${sep}per_page=100`
|
|
68
|
+
dlog(`projectScopedRequest uri: ${uri}`)
|
|
68
69
|
const headers = new Headers()
|
|
69
|
-
headers.append("Accept", "application/json")
|
|
70
70
|
headers.append('Private-Token', token)
|
|
71
71
|
const options = {
|
|
72
72
|
method,
|
|
73
73
|
headers,
|
|
74
74
|
}
|
|
75
75
|
const request = new Request(uri, options)
|
|
76
|
-
|
|
76
|
+
return await fetch(request)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function projectScopedGet(endpoint: string): Promise<JSONValue> {
|
|
80
|
+
const response = await projectScopedRequest(endpoint)
|
|
77
81
|
return await response.json()
|
|
78
82
|
}
|
|
83
|
+
|
|
84
|
+
export async function projectScopedGetText(endpoint: string): Promise<string> {
|
|
85
|
+
const response = await projectScopedRequest(endpoint)
|
|
86
|
+
return await response.text()
|
|
87
|
+
}
|
package/lib/help.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
|
|
3
|
+
// Recursively format a Commander command tree using box-drawing characters
|
|
4
|
+
export function formatCommandTree(cmd: Command): string {
|
|
5
|
+
const lines: Array<string> = []
|
|
6
|
+
const name = cmd.name() || 'gitj'
|
|
7
|
+
lines.push(name)
|
|
8
|
+
appendChildren(lines, cmd, '')
|
|
9
|
+
return lines.join('\n')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function appendChildren(lines: Array<string>, cmd: Command, prefix: string): void {
|
|
13
|
+
const children = cmd.commands as Array<Command>
|
|
14
|
+
if (!children || children.length === 0) return
|
|
15
|
+
|
|
16
|
+
const descWidth = longestNameChain(children)
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < children.length; i++) {
|
|
19
|
+
const child = children[i]
|
|
20
|
+
const isLast = i === children.length - 1
|
|
21
|
+
const connector = isLast ? '└── ' : '├── '
|
|
22
|
+
const childPrefix = isLast ? ' ' : '│ '
|
|
23
|
+
const name = child.name()
|
|
24
|
+
const desc = child.description()
|
|
25
|
+
const padding = desc ? ' '.repeat(Math.max(1, descWidth - name.length + 2)) : ''
|
|
26
|
+
const descPart = desc ? padding + desc : ''
|
|
27
|
+
lines.push(`${prefix}${connector}${name}${descPart}`)
|
|
28
|
+
appendChildren(lines, child, prefix + childPrefix)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Find the longest command name among siblings for alignment
|
|
33
|
+
function longestNameChain(commands: Array<Command>): number {
|
|
34
|
+
let max = 0
|
|
35
|
+
for (const cmd of commands) {
|
|
36
|
+
const len = cmd.name().length
|
|
37
|
+
if (len > max) max = len
|
|
38
|
+
}
|
|
39
|
+
return max
|
|
40
|
+
}
|
package/lib/jira.ts
CHANGED
|
@@ -62,7 +62,7 @@ type Myself = JSONValue & {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export async function getMyself(): Promise<Myself> {
|
|
65
|
-
return await jiraApi("
|
|
65
|
+
return await jiraApi("myself") as Myself
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
type SearchResponse = JSONValue & {
|
|
@@ -73,6 +73,6 @@ export async function myUnresolvedIssues(): Promise<Array<Issue>> {
|
|
|
73
73
|
const myself = await getMyself()
|
|
74
74
|
const myselfId = myself.accountId
|
|
75
75
|
const jql = `assignee = ${myselfId} AND resolution = Unresolved`
|
|
76
|
-
const issues = await jiraApi(
|
|
76
|
+
const issues = await jiraApi(`search/jql?jql=${encodeURIComponent(jql)}&fields=summary`) as SearchResponse
|
|
77
77
|
return issues.issues
|
|
78
78
|
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ya-git-jira",
|
|
3
3
|
"description": "git extensions for Jira integration. Assumes bun is installed.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
+
"git-api": "dist/bin/git-api.js",
|
|
8
9
|
"git-bump": "dist/bin/git-bump.js",
|
|
10
|
+
"git-confluence-page-search": "dist/bin/git-confluence-page-search.js",
|
|
11
|
+
"git-confluence-page-show": "dist/bin/git-confluence-page-show.js",
|
|
12
|
+
"git-confluence-page-update": "dist/bin/git-confluence-page-update.js",
|
|
13
|
+
"git-confluence-page": "dist/bin/git-confluence-page.js",
|
|
14
|
+
"git-confluence-space-list": "dist/bin/git-confluence-space-list.js",
|
|
15
|
+
"git-confluence-space": "dist/bin/git-confluence-space.js",
|
|
16
|
+
"git-confluence-whoami": "dist/bin/git-confluence-whoami.js",
|
|
17
|
+
"git-confluence": "dist/bin/git-confluence.js",
|
|
9
18
|
"git-jira-issue-list": "dist/bin/git-jira-issue-list.js",
|
|
10
19
|
"git-jira-issue-show": "dist/bin/git-jira-issue-show.js",
|
|
11
20
|
"git-jira-issue": "dist/bin/git-jira-issue.js",
|
|
12
21
|
"git-jira-start": "dist/bin/git-jira-start.js",
|
|
22
|
+
"git-jira-whoami": "dist/bin/git-jira-whoami.js",
|
|
13
23
|
"git-jira": "dist/bin/git-jira.js",
|
|
14
24
|
"git-lab-group-list": "dist/bin/git-lab-group-list.js",
|
|
15
25
|
"git-lab-group": "dist/bin/git-lab-group.js",
|
|
@@ -21,13 +31,19 @@
|
|
|
21
31
|
"git-lab-namespace-list": "dist/bin/git-lab-namespace-list.js",
|
|
22
32
|
"git-lab-namespace": "dist/bin/git-lab-namespace.js",
|
|
23
33
|
"git-lab-project-list": "dist/bin/git-lab-project-list.js",
|
|
34
|
+
"git-lab-project-mr": "dist/bin/git-lab-project-mr.js",
|
|
35
|
+
"git-lab-project-mr-list": "dist/bin/git-lab-project-mr-list.js",
|
|
24
36
|
"git-lab-project-pipeline": "dist/bin/git-lab-project-pipeline.js",
|
|
37
|
+
"git-lab-project-pipeline-jobs": "dist/bin/git-lab-project-pipeline-jobs.js",
|
|
38
|
+
"git-lab-project-pipeline-latest": "dist/bin/git-lab-project-pipeline-latest.js",
|
|
25
39
|
"git-lab-project-pipeline-list": "dist/bin/git-lab-project-pipeline-list.js",
|
|
40
|
+
"git-lab-project-pipeline-log": "dist/bin/git-lab-project-pipeline-log.js",
|
|
26
41
|
"git-lab-project-whereami": "dist/bin/git-lab-project-whereami.js",
|
|
27
42
|
"git-lab-project": "dist/bin/git-lab-project.js",
|
|
28
43
|
"git-lab-whoami": "dist/bin/git-lab-whoami.js",
|
|
29
44
|
"git-lab": "dist/bin/git-lab.js",
|
|
30
|
-
"gitj": "dist/bin/gitj.js"
|
|
45
|
+
"gitj": "dist/bin/gitj.js",
|
|
46
|
+
"gitj-install-skills": "dist/bin/gitj-install-skills.js"
|
|
31
47
|
},
|
|
32
48
|
"scripts": {
|
|
33
49
|
"preinstall": "bun run build.ts",
|