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.
Files changed (86) hide show
  1. package/.opencode/skills/architecture/SKILL.md +45 -0
  2. package/.opencode/skills/code-style/SKILL.md +76 -0
  3. package/.opencode/skills/git-confluence/SKILL.md +82 -0
  4. package/.opencode/skills/git-jira/SKILL.md +63 -0
  5. package/.opencode/skills/git-lab/SKILL.md +102 -0
  6. package/AGENTS.md +50 -0
  7. package/README.md +106 -106
  8. package/bin/git-api.ts +70 -0
  9. package/bin/git-confluence-page-search.ts +58 -0
  10. package/bin/git-confluence-page-show.ts +61 -0
  11. package/bin/git-confluence-page-update.ts +77 -0
  12. package/bin/git-confluence-page.ts +28 -0
  13. package/bin/git-confluence-space-list.ts +34 -0
  14. package/bin/git-confluence-space.ts +24 -0
  15. package/bin/git-confluence-whoami.ts +33 -0
  16. package/bin/git-confluence.ts +27 -0
  17. package/bin/git-jira-start.ts +1 -1
  18. package/bin/git-jira-whoami.ts +32 -0
  19. package/bin/git-jira.ts +2 -0
  20. package/bin/git-lab-project-mr-list.ts +57 -0
  21. package/bin/git-lab-project-mr.ts +24 -0
  22. package/bin/git-lab-project-pipeline-jobs.ts +46 -0
  23. package/bin/git-lab-project-pipeline-latest.ts +47 -0
  24. package/bin/git-lab-project-pipeline-log.ts +49 -0
  25. package/bin/git-lab-project-pipeline.ts +6 -0
  26. package/bin/git-lab-project.ts +5 -1
  27. package/bin/gitj-install-skills.ts +126 -0
  28. package/bin/gitj.ts +12 -0
  29. package/dist/bin/git-api.js +2156 -0
  30. package/dist/bin/git-bump.js +132 -121
  31. package/dist/bin/git-confluence-page-search.js +2079 -0
  32. package/dist/bin/git-confluence-page-show.js +2082 -0
  33. package/dist/bin/git-confluence-page-update.js +2093 -0
  34. package/dist/bin/git-confluence-page.js +2186 -0
  35. package/dist/bin/git-confluence-space-list.js +2061 -0
  36. package/dist/bin/git-confluence-space.js +2073 -0
  37. package/dist/bin/git-confluence-whoami.js +2060 -0
  38. package/dist/bin/git-confluence.js +2251 -0
  39. package/dist/bin/git-jira-issue-list.js +136 -125
  40. package/dist/bin/git-jira-issue-show.js +136 -125
  41. package/dist/bin/git-jira-issue.js +140 -129
  42. package/dist/bin/git-jira-start.js +138 -127
  43. package/dist/bin/git-jira-whoami.js +1972 -0
  44. package/dist/bin/git-jira.js +170 -139
  45. package/dist/bin/git-lab-group-list.js +321 -279
  46. package/dist/bin/git-lab-group.js +323 -281
  47. package/dist/bin/git-lab-merge-active.js +321 -279
  48. package/dist/bin/git-lab-merge-todo.js +321 -279
  49. package/dist/bin/git-lab-merge-train-list.js +289 -273
  50. package/dist/bin/git-lab-merge-train.js +291 -275
  51. package/dist/bin/git-lab-merge.js +330 -288
  52. package/dist/bin/git-lab-namespace-list.js +138 -127
  53. package/dist/bin/git-lab-namespace.js +140 -129
  54. package/dist/bin/git-lab-project-list.js +288 -272
  55. package/dist/bin/git-lab-project-mr-list.js +2740 -0
  56. package/dist/bin/git-lab-project-mr.js +2752 -0
  57. package/dist/bin/git-lab-project-pipeline-jobs.js +2734 -0
  58. package/dist/bin/git-lab-project-pipeline-latest.js +2736 -0
  59. package/dist/bin/git-lab-project-pipeline-list.js +323 -281
  60. package/dist/bin/git-lab-project-pipeline-log.js +2739 -0
  61. package/dist/bin/git-lab-project-pipeline.js +437 -292
  62. package/dist/bin/git-lab-project-whereami.js +292 -276
  63. package/dist/bin/git-lab-project.js +563 -288
  64. package/dist/bin/git-lab-whoami.js +142 -131
  65. package/dist/bin/git-lab.js +575 -338
  66. package/dist/bin/gitj-install-skills.js +1954 -0
  67. package/dist/bin/gitj.js +1385 -473
  68. package/dist/index.js +371 -187
  69. package/index.ts +1 -0
  70. package/lib/api.ts +177 -0
  71. package/lib/confluence/api.ts +132 -0
  72. package/lib/confluence/config.ts +25 -0
  73. package/lib/confluence/index.ts +3 -0
  74. package/lib/confluence/types.ts +59 -0
  75. package/lib/gitlab/index.ts +1 -0
  76. package/lib/gitlab/job.ts +31 -0
  77. package/lib/gitlab/merge-request.ts +20 -0
  78. package/lib/gitlab/pipeline.ts +28 -1
  79. package/lib/gitlab/project.ts +14 -5
  80. package/lib/help.ts +40 -0
  81. package/lib/jira.ts +2 -2
  82. package/package.json +18 -2
  83. package/tests/all-help.test.ts +6 -1
  84. package/tests/gitj.test.ts +1 -1
  85. package/tests/help-all.test.ts +29 -0
  86. package/bun.lockb +0 -0
package/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./lib/json.ts"
2
+ export * from "./lib/confluence"
2
3
  export * from "./lib/git.ts"
3
4
  export * from "./lib/gitlab"
4
5
  export * from "./lib/is_main.ts"
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,3 @@
1
+ export * from './api'
2
+ export * from './config'
3
+ export * from './types'
@@ -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
+ }
@@ -1,6 +1,7 @@
1
1
  export * from './api'
2
2
  export * from './config'
3
3
  export * from './group'
4
+ export * from './job'
4
5
  export * from './merge-request'
5
6
  export * from './namespace'
6
7
  export * from './pipeline'
@@ -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
+ }
@@ -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
+ }
@@ -50,7 +50,7 @@ export async function findProject(ssh_url: string): Promise<Project | undefined>
50
50
  return project
51
51
  }
52
52
 
53
- export async function projectScopedGet(endpoint: string): Promise<JSONValue> {
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 uri = `${base}/${endpoint}`
67
- dlog(`projectScopedGet uri: ${uri}`)
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
- const response = await fetch(request)
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("/myself") as Myself
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(`/search?jql=${encodeURIComponent(jql)}`) as SearchResponse
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": "1.6.0",
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",