@baiyibai-antora/jira-retriever 1.0.0-beta.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/bin/jira-retrieve.js +106 -0
- package/lib/index.js +5 -0
- package/lib/retrieve.js +277 -0
- package/package.json +43 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// jira-retrieve: pull Jira issues selected by JQL down as AsciiDoc files.
|
|
5
|
+
// Configuration via jira.yml and/or flags; credentials from the environment.
|
|
6
|
+
|
|
7
|
+
const fsp = require('node:fs/promises')
|
|
8
|
+
const ospath = require('node:path')
|
|
9
|
+
const yaml = require('js-yaml')
|
|
10
|
+
const { AtlassianClient, credentialsFromEnv } = require('@baiyibai-antora/atlassian-client')
|
|
11
|
+
const { retrieveIssues, DEFAULT_FIELDS } = require('../lib')
|
|
12
|
+
|
|
13
|
+
const USAGE = `Usage: jira-retrieve [options] [jira.yml]
|
|
14
|
+
|
|
15
|
+
Writes one .adoc file per issue (KEY.adoc): scalar fields become header
|
|
16
|
+
attributes, the description becomes the body, comments and other rich-text
|
|
17
|
+
fields become sections.
|
|
18
|
+
|
|
19
|
+
jira.yml:
|
|
20
|
+
jql: project = DEMO ORDER BY key
|
|
21
|
+
fields: [summary, description, labels, status, comment] # ids or names
|
|
22
|
+
out: issues
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--jql <query> JQL (overrides the config file)
|
|
26
|
+
--fields <a,b,c> fields to retrieve (default: ${DEFAULT_FIELDS.join(',')})
|
|
27
|
+
--out <dir> output directory (default: ./issues)
|
|
28
|
+
--keep-colors keep text/background colors as role spans
|
|
29
|
+
-h, --help show this help
|
|
30
|
+
|
|
31
|
+
Environment: ATLASSIAN_BASE_URL (or JIRA_BASE_URL), ATLASSIAN_EMAIL,
|
|
32
|
+
ATLASSIAN_API_TOKEN`
|
|
33
|
+
|
|
34
|
+
function parseArgs (argv) {
|
|
35
|
+
const opts = { jql: null, fields: null, out: null, styles: 'drop', config: null }
|
|
36
|
+
for (let i = 0; i < argv.length; i++) {
|
|
37
|
+
const arg = argv[i]
|
|
38
|
+
if (arg === '-h' || arg === '--help') return { help: true }
|
|
39
|
+
else if (arg === '--jql') opts.jql = argv[++i]
|
|
40
|
+
else if (arg === '--fields') opts.fields = argv[++i].split(',').map((f) => f.trim()).filter(Boolean)
|
|
41
|
+
else if (arg === '--out') opts.out = argv[++i]
|
|
42
|
+
else if (arg === '--keep-colors') opts.styles = 'roles'
|
|
43
|
+
else if (arg.startsWith('-')) throw new Error(`unknown option ${arg}`)
|
|
44
|
+
else if (opts.config) throw new Error('only one config file may be given')
|
|
45
|
+
else opts.config = arg
|
|
46
|
+
}
|
|
47
|
+
return opts
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function main () {
|
|
51
|
+
let opts
|
|
52
|
+
try {
|
|
53
|
+
opts = parseArgs(process.argv.slice(2))
|
|
54
|
+
} catch (err) {
|
|
55
|
+
process.stderr.write(`jira-retrieve: ${err.message}\n${USAGE}\n`)
|
|
56
|
+
return 1
|
|
57
|
+
}
|
|
58
|
+
if (opts.help) {
|
|
59
|
+
process.stdout.write(USAGE + '\n')
|
|
60
|
+
return 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let config = {}
|
|
64
|
+
if (opts.config) config = yaml.load(await fsp.readFile(opts.config, 'utf8')) || {}
|
|
65
|
+
const jql = opts.jql || config.jql
|
|
66
|
+
if (!jql) {
|
|
67
|
+
process.stderr.write(`jira-retrieve: no JQL given (pass --jql or a config file)\n${USAGE}\n`)
|
|
68
|
+
return 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let client
|
|
72
|
+
try {
|
|
73
|
+
client = new AtlassianClient({ ...credentialsFromEnv(), serviceName: 'Jira' })
|
|
74
|
+
} catch (err) {
|
|
75
|
+
process.stderr.write(`jira-retrieve: ${err.message}\n`)
|
|
76
|
+
return 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const logger = { warn: (msg) => process.stderr.write(`jira-retrieve: ${msg}\n`) }
|
|
80
|
+
const { files, issues } = await retrieveIssues({
|
|
81
|
+
client,
|
|
82
|
+
jql,
|
|
83
|
+
fields: opts.fields || config.fields || undefined,
|
|
84
|
+
styles: opts.styles,
|
|
85
|
+
logger,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const outDir = opts.out || config.out || 'issues'
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
const dest = ospath.join(outDir, ...file.path.split('/'))
|
|
91
|
+
await fsp.mkdir(ospath.dirname(dest), { recursive: true })
|
|
92
|
+
await fsp.writeFile(dest, file.contents)
|
|
93
|
+
}
|
|
94
|
+
process.stdout.write(`retrieved ${issues.length} issue(s) to ${outDir}\n`)
|
|
95
|
+
return 0
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
main().then(
|
|
99
|
+
(code) => {
|
|
100
|
+
process.exitCode = code
|
|
101
|
+
},
|
|
102
|
+
(err) => {
|
|
103
|
+
process.stderr.write(`jira-retrieve: ${err.message}\n`)
|
|
104
|
+
process.exitCode = 1
|
|
105
|
+
}
|
|
106
|
+
)
|
package/lib/index.js
ADDED
package/lib/retrieve.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { posix: path } = require('node:path')
|
|
4
|
+
const { toAdoc } = require('@baiyibai-antora/asciidoc-adf-converter')
|
|
5
|
+
|
|
6
|
+
// Retrieve Jira issues selected by JQL as an in-memory AsciiDoc file tree,
|
|
7
|
+
// one file per issue, in the tagged issue format (see the README): the
|
|
8
|
+
// issue is a level-1 section carrying its scalar fields as named block
|
|
9
|
+
// attributes, the description is the section body, and comments, custom
|
|
10
|
+
// rich-text fields, and attachments are tagged subsections. Everything
|
|
11
|
+
// semantic is a walkable node attribute — no document attributes, so files
|
|
12
|
+
// can be concatenated or included without collisions, and jira-publish can
|
|
13
|
+
// consume them.
|
|
14
|
+
//
|
|
15
|
+
// The v3 REST API returns rich-text fields as ADF. Attachments download
|
|
16
|
+
// into _attachments/KEY/. Inline media in ADF reference Media Services
|
|
17
|
+
// UUIDs that no v3 payload maps to attachments; the bridge is the rendered
|
|
18
|
+
// HTML (expand=renderedFields), where the same images appear in document
|
|
19
|
+
// order as /attachment/content/<id> URLs. When the counts line up the
|
|
20
|
+
// media resolve to the downloaded files; otherwise they stay adf-media:
|
|
21
|
+
// placeholders with a warning.
|
|
22
|
+
|
|
23
|
+
const RICH_DEFAULT = [
|
|
24
|
+
'summary', 'description', 'issuetype', 'status', 'priority', 'assignee',
|
|
25
|
+
'reporter', 'labels', 'components', 'fixVersions', 'parent', 'created',
|
|
26
|
+
'updated', 'comment', 'attachment', 'issuelinks', 'subtasks',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
// Noise dropped when `fields: all` expands to *all.
|
|
30
|
+
const ALL_SKIP = new Set([
|
|
31
|
+
'avatarUrls', 'votes', 'watches', 'worklog', 'progress', 'aggregateprogress',
|
|
32
|
+
'aggregatetimeestimate', 'aggregatetimeoriginalestimate', 'aggregatetimespent',
|
|
33
|
+
'timeestimate', 'timeoriginalestimate', 'timespent', 'timetracking',
|
|
34
|
+
'workratio', 'lastViewed', 'statuscategorychangedate', 'thumbnail',
|
|
35
|
+
'security', 'environment',
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
const isAdfDoc = (value) => Boolean(value) && typeof value === 'object' && value.type === 'doc'
|
|
39
|
+
|
|
40
|
+
function attrValue (value) {
|
|
41
|
+
if (value == null) return null
|
|
42
|
+
if (Array.isArray(value)) return value.map(attrValue).filter((v) => v != null).join(', ')
|
|
43
|
+
if (typeof value === 'object') return value.name || value.displayName || value.key || value.value || null
|
|
44
|
+
return String(value)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function attrName (field) {
|
|
48
|
+
return String(field)
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
51
|
+
.replace(/^-+|-+$/g, '')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// One `name=value` pair for an AsciiDoc attribute list: quoted unless the
|
|
55
|
+
// value is plainly safe, newlines flattened.
|
|
56
|
+
function attrPair (name, value) {
|
|
57
|
+
let v = String(value).replace(/\s*\n\s*/g, ' ').trim()
|
|
58
|
+
if (/[^A-Za-z0-9_.:@/-]/.test(v)) v = `"${v.replace(/"/g, '\\"')}"`
|
|
59
|
+
return `${name}=${v}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// System fields keep predictable attribute names regardless of how the
|
|
63
|
+
// site's field catalog displays them; custom fields derive from their name.
|
|
64
|
+
const SYSTEM_ATTR = {
|
|
65
|
+
issuetype: 'jira-issuetype',
|
|
66
|
+
status: 'jira-status',
|
|
67
|
+
priority: 'jira-priority',
|
|
68
|
+
labels: 'jira-labels',
|
|
69
|
+
components: 'jira-components',
|
|
70
|
+
fixVersions: 'jira-fix-versions',
|
|
71
|
+
created: 'jira-created',
|
|
72
|
+
updated: 'jira-updated',
|
|
73
|
+
resolution: 'jira-resolution',
|
|
74
|
+
duedate: 'jira-duedate',
|
|
75
|
+
project: 'jira-project',
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function resolveFields (client, wanted) {
|
|
79
|
+
const catalog = await client.request('GET', '/rest/api/3/field')
|
|
80
|
+
const byName = new Map((catalog || []).map((f) => [f.name.toLowerCase(), f]))
|
|
81
|
+
const byId = new Map((catalog || []).map((f) => [f.id, f]))
|
|
82
|
+
return wanted.map((entry) => {
|
|
83
|
+
const field = byId.get(entry) || byName.get(String(entry).toLowerCase())
|
|
84
|
+
if (!field) throw new Error(`unknown Jira field "${entry}" (use an id or the exact display name)`)
|
|
85
|
+
return { id: field.id, name: field.name }
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Media Services UUIDs (ADF, document order) zipped against the attachment
|
|
90
|
+
// content ids in the rendered HTML of the same body.
|
|
91
|
+
function walkMediaIds (node, out = []) {
|
|
92
|
+
if (!node || typeof node !== 'object') return out
|
|
93
|
+
if (node.type === 'media' || node.type === 'mediaInline') {
|
|
94
|
+
if ((node.attrs || {}).type === 'file' && node.attrs.id) out.push(node.attrs.id)
|
|
95
|
+
}
|
|
96
|
+
for (const child of node.content || []) walkMediaIds(child, out)
|
|
97
|
+
return out
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function mediaMapFor (adf, renderedHtml, attachmentsById, warn, label) {
|
|
101
|
+
const uuids = walkMediaIds(adf)
|
|
102
|
+
if (!uuids.length) return new Map()
|
|
103
|
+
const numeric = [...String(renderedHtml || '').matchAll(/\/attachment\/(?:content|thumbnail)\/(\d+)/g)].map((m) => m[1])
|
|
104
|
+
if (numeric.length !== uuids.length) {
|
|
105
|
+
warn(`${label}: ${uuids.length} media node(s) but ${numeric.length} attachment reference(s) in the rendered body; leaving adf-media placeholders`)
|
|
106
|
+
return new Map()
|
|
107
|
+
}
|
|
108
|
+
const map = new Map()
|
|
109
|
+
uuids.forEach((uuid, i) => {
|
|
110
|
+
const att = attachmentsById.get(numeric[i])
|
|
111
|
+
if (att) map.set(uuid, att)
|
|
112
|
+
})
|
|
113
|
+
return map
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function issueToAdoc (issue, resolved, opts, warn) {
|
|
117
|
+
const { styles, attachmentDir, attachmentFiles } = opts
|
|
118
|
+
const values = issue.fields || {}
|
|
119
|
+
const rendered = issue.renderedFields || {}
|
|
120
|
+
const key = issue.key
|
|
121
|
+
const attachments = Array.isArray(values.attachment) ? values.attachment : []
|
|
122
|
+
const attachmentsById = new Map(attachments.map((att) => [String(att.id), att]))
|
|
123
|
+
|
|
124
|
+
const convert = (adf, label, renderedHtml, levelOffset) => {
|
|
125
|
+
const mediaMap = mediaMapFor(adf, renderedHtml, attachmentsById, warn, `${key} ${label}`)
|
|
126
|
+
const resolveMedia = ({ id }) => {
|
|
127
|
+
const att = mediaMap.get(String(id))
|
|
128
|
+
if (!att) return null
|
|
129
|
+
attachmentFiles.add(String(att.id))
|
|
130
|
+
return path.join(attachmentDir, att.filename)
|
|
131
|
+
}
|
|
132
|
+
const result = toAdoc(adf, { styles, levelOffset, resolveMedia })
|
|
133
|
+
for (const warning of result.warnings) warn(`${key} ${label}: ${warning}`)
|
|
134
|
+
return result.adoc.trimEnd()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const attrs = [attrPair('jira', 'issue'), attrPair('jira-key', key)]
|
|
138
|
+
if (issue.id) attrs.push(attrPair('jira-id', issue.id))
|
|
139
|
+
attrs.push(attrPair('jira-project', key.replace(/-\d+$/, '')))
|
|
140
|
+
const sections = []
|
|
141
|
+
let description = ''
|
|
142
|
+
|
|
143
|
+
const push = (name, value) => {
|
|
144
|
+
const v = attrValue(value)
|
|
145
|
+
if (v != null && v !== '') attrs.push(attrPair(name, v))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const field of resolved) {
|
|
149
|
+
const value = values[field.id]
|
|
150
|
+
if (field.id === 'summary' || value == null) continue
|
|
151
|
+
switch (field.id) {
|
|
152
|
+
case 'description':
|
|
153
|
+
if (isAdfDoc(value)) description = convert(value, 'description', rendered.description, 1)
|
|
154
|
+
break
|
|
155
|
+
case 'comment': {
|
|
156
|
+
for (const comment of (value.comments || []).filter((c) => isAdfDoc(c.body))) {
|
|
157
|
+
const author = (comment.author && comment.author.displayName) || 'unknown'
|
|
158
|
+
const date = comment.created ? String(comment.created).slice(0, 10) : ''
|
|
159
|
+
const cAttrs = [attrPair('jira', 'comment'), attrPair('jira-id', comment.id)]
|
|
160
|
+
cAttrs.push(attrPair('jira-author', author))
|
|
161
|
+
if (comment.author && comment.author.accountId) cAttrs.push(attrPair('jira-author-id', comment.author.accountId))
|
|
162
|
+
if (comment.created) cAttrs.push(attrPair('jira-created', comment.created))
|
|
163
|
+
const renderedBody = ((rendered.comment || {}).comments || []).find((c) => String(c.id) === String(comment.id))
|
|
164
|
+
sections.push(
|
|
165
|
+
`[${cAttrs.join(',')}]\n=== ${author}${date ? ` (${date})` : ''}\n\n` +
|
|
166
|
+
convert(comment.body, `comment ${comment.id}`, renderedBody && renderedBody.body, 2)
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
break
|
|
170
|
+
}
|
|
171
|
+
case 'attachment': {
|
|
172
|
+
if (!attachments.length) break
|
|
173
|
+
const items = attachments.map((att) => {
|
|
174
|
+
attachmentFiles.add(String(att.id))
|
|
175
|
+
const target = path.join(attachmentDir, att.filename)
|
|
176
|
+
const size = att.size >= 1024 ? `${Math.round(att.size / 1024)} KB` : `${att.size} B`
|
|
177
|
+
const author = (att.author && att.author.displayName) || 'unknown'
|
|
178
|
+
const date = att.created ? String(att.created).slice(0, 10) : ''
|
|
179
|
+
return `* link:${target}[${att.filename}] (${att.mimeType || 'unknown'}, ${size}, ${author}${date ? `, ${date}` : ''})`
|
|
180
|
+
})
|
|
181
|
+
sections.push(`[jira=attachments]\n=== Attachments\n\n${items.join('\n')}`)
|
|
182
|
+
break
|
|
183
|
+
}
|
|
184
|
+
case 'assignee':
|
|
185
|
+
case 'reporter':
|
|
186
|
+
push(`jira-${field.id}`, value.displayName)
|
|
187
|
+
if (value.accountId) push(`jira-${field.id}-id`, value.accountId)
|
|
188
|
+
break
|
|
189
|
+
case 'parent':
|
|
190
|
+
push('jira-parent', value.key)
|
|
191
|
+
break
|
|
192
|
+
case 'issuelinks': {
|
|
193
|
+
const links = value
|
|
194
|
+
.map((link) => {
|
|
195
|
+
if (link.outwardIssue) return `${link.type.outward} ${link.outwardIssue.key}`
|
|
196
|
+
if (link.inwardIssue) return `${link.type.inward} ${link.inwardIssue.key}`
|
|
197
|
+
return null
|
|
198
|
+
})
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
if (links.length) attrs.push(attrPair('jira-links', links.join('; ')))
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
case 'subtasks':
|
|
204
|
+
if (value.length) push('jira-subtasks', value.map((s) => s.key).join(', '))
|
|
205
|
+
break
|
|
206
|
+
default:
|
|
207
|
+
if (isAdfDoc(value)) {
|
|
208
|
+
sections.push(
|
|
209
|
+
`[${attrPair('jira', 'field')},${attrPair('jira-field-name', field.name)}]\n=== ${field.name}\n\n` +
|
|
210
|
+
convert(value, field.name, rendered[field.id], 2)
|
|
211
|
+
)
|
|
212
|
+
} else {
|
|
213
|
+
push(SYSTEM_ATTR[field.id] || `jira-${attrName(field.name)}`, value)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const summary = values.summary || key
|
|
219
|
+
const parts = [`[${attrs.join(',')}]\n== ${key}: ${summary}`, description, ...sections].filter(Boolean)
|
|
220
|
+
return parts.join('\n\n') + '\n'
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function retrieveIssues ({ client, jql, fields, styles, logger }) {
|
|
224
|
+
const warnings = []
|
|
225
|
+
const warn = (msg) => {
|
|
226
|
+
warnings.push(msg)
|
|
227
|
+
if (logger) logger.warn(msg)
|
|
228
|
+
}
|
|
229
|
+
if (!jql) throw new Error('a jql query is required')
|
|
230
|
+
|
|
231
|
+
let resolved
|
|
232
|
+
let fieldParam
|
|
233
|
+
if (fields === 'all' || (Array.isArray(fields) && fields.length === 1 && fields[0] === 'all')) {
|
|
234
|
+
const catalog = await client.request('GET', '/rest/api/3/field')
|
|
235
|
+
resolved = (catalog || [])
|
|
236
|
+
.filter((f) => !ALL_SKIP.has(f.id))
|
|
237
|
+
.map((f) => ({ id: f.id, name: f.name }))
|
|
238
|
+
fieldParam = '*all'
|
|
239
|
+
} else {
|
|
240
|
+
resolved = await resolveFields(client, fields || RICH_DEFAULT)
|
|
241
|
+
if (!resolved.some((f) => f.id === 'summary')) resolved.unshift({ id: 'summary', name: 'Summary' })
|
|
242
|
+
fieldParam = resolved.map((f) => f.id).join(',')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const files = []
|
|
246
|
+
const issues = []
|
|
247
|
+
let pageToken
|
|
248
|
+
do {
|
|
249
|
+
const params = new URLSearchParams({ jql, fields: fieldParam, maxResults: '100', expand: 'renderedFields' })
|
|
250
|
+
if (pageToken) params.set('nextPageToken', pageToken)
|
|
251
|
+
const page = await client.request('GET', `/rest/api/3/search/jql?${params}`)
|
|
252
|
+
for (const issue of (page && page.issues) || []) {
|
|
253
|
+
const attachmentDir = path.join('_attachments', issue.key)
|
|
254
|
+
const attachmentFiles = new Set()
|
|
255
|
+
const contents = issueToAdoc(issue, resolved, { styles, attachmentDir, attachmentFiles }, warn)
|
|
256
|
+
|
|
257
|
+
for (const att of (issue.fields || {}).attachment || []) {
|
|
258
|
+
if (!attachmentFiles.has(String(att.id)) || !att.content) continue
|
|
259
|
+
const bytes = await client.download(String(att.content).replace(client.baseUrl, ''))
|
|
260
|
+
if (bytes) {
|
|
261
|
+
files.push({ path: path.join(attachmentDir, att.filename), contents: bytes })
|
|
262
|
+
} else {
|
|
263
|
+
warn(`${issue.key}: attachment ${att.filename} could not be downloaded`)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const filePath = `${issue.key}.adoc`
|
|
268
|
+
files.push({ path: filePath, contents })
|
|
269
|
+
issues.push({ key: issue.key, path: filePath, summary: (issue.fields || {}).summary || issue.key })
|
|
270
|
+
}
|
|
271
|
+
pageToken = page && page.nextPageToken
|
|
272
|
+
} while (pageToken)
|
|
273
|
+
|
|
274
|
+
return { files, issues, warnings }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = { retrieveIssues, DEFAULT_FIELDS: RICH_DEFAULT }
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@baiyibai-antora/jira-retriever",
|
|
3
|
+
"version": "1.0.0-beta.5",
|
|
4
|
+
"description": "Retrieve Jira Cloud issues as AsciiDoc: JQL selection over the v3 REST API, rich-text fields arrive as ADF and convert with @baiyibai-antora/asciidoc-adf-converter",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "baiyibai",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://gitlab.com/baiyibai-antora/confluora.git",
|
|
10
|
+
"directory": "packages/jira-retriever"
|
|
11
|
+
},
|
|
12
|
+
"type": "commonjs",
|
|
13
|
+
"main": "lib/index.js",
|
|
14
|
+
"bin": {
|
|
15
|
+
"jira-retrieve": "bin/jira-retrieve.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"lib",
|
|
19
|
+
"bin"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --test test/*-test.js"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@baiyibai-antora/asciidoc-adf-converter": "^1.0.0-beta.5",
|
|
26
|
+
"@baiyibai-antora/atlassian-client": "^1.0.0-beta.5",
|
|
27
|
+
"js-yaml": "^4.1.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"confluora",
|
|
31
|
+
"jira",
|
|
32
|
+
"asciidoc",
|
|
33
|
+
"adf",
|
|
34
|
+
"issues",
|
|
35
|
+
"retriever"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|