kup-cli 0.1.4 → 0.2.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/README.md +16 -1
- package/README.zh.md +16 -1
- package/bin/cli.js +8 -1
- package/lib/file.js +10 -0
- package/lib/main.js +58 -18
- package/lib/meta.js +86 -9
- package/lib/parse.js +6 -4
- package/lib/sync.js +97 -9
- package/lib/validate.js +22 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,20 @@ kup ./path/to/file.md --repo foo/bar --id 123
|
|
|
44
44
|
|
|
45
45
|
Kup will update issue `123` in the `foo/bar` repository with the content of `file.md`.
|
|
46
46
|
|
|
47
|
+
### Dump an Existing Issue
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
kup --dump --repo foo/bar --id 123
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Kup will dump issue `123` from the `foo/bar` repository to `123.md` in the current directory.
|
|
54
|
+
|
|
55
|
+
If you want to specify the output file, you can also use:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
kup ./path/to/file.md --dump --repo foo/bar --id 123
|
|
59
|
+
```
|
|
60
|
+
|
|
47
61
|
### Preparation
|
|
48
62
|
|
|
49
63
|
Updating GitHub issues requires authentication, so you need to provide a GitHub token for Kup to call the GitHub API.
|
|
@@ -65,6 +79,7 @@ Option | Short | Value Type | Description | Notes
|
|
|
65
79
|
---|---|---|---|---
|
|
66
80
|
`--repo` | `-r` | string | Specify the GitHub repository
|
|
67
81
|
`--id` | `-i` | integer | Specify the issue number | <ul><li>Providing a number means updating an existing issue<li>Omitting it means publishing a new issue</ul>
|
|
82
|
+
`--dump` | `-d` | - | Enable dump mode, save a GitHub issue as a local Markdown file
|
|
68
83
|
`--version` | `-v` | - | Show the version number
|
|
69
84
|
`--help` | `-h` | - | Show help information
|
|
70
85
|
|
|
@@ -90,7 +105,7 @@ Kup can determine the `id` option in the following order of priority:
|
|
|
90
105
|
1. The `--id` option passed on the command line.
|
|
91
106
|
1. The `id` field in the Markdown file's metadata.
|
|
92
107
|
|
|
93
|
-
After a file is successfully published as an issue, Kup
|
|
108
|
+
After a file is successfully published as an issue, Kup writes the `id` back to the file’s metadata (with user confirmation before writing).
|
|
94
109
|
|
|
95
110
|
### How is the issue title determined?
|
|
96
111
|
|
package/README.zh.md
CHANGED
|
@@ -44,6 +44,20 @@ kup ./path/to/file.md --repo foo/bar --id 123
|
|
|
44
44
|
|
|
45
45
|
Kup 会把 `file.md` 文件的内容更新到 `foo/bar` 仓库的编号为 `123` 的 issue。
|
|
46
46
|
|
|
47
|
+
### 采集已有 issue
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
kup --dump --repo foo/bar --id 123
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Kup 会把 `foo/bar` 仓库的编号为 `123` 的 issue 采集为当前目录下的 `123.md` 文件。
|
|
54
|
+
|
|
55
|
+
如果你想指定输出文件,也可以这样使用:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
kup ./path/to/file.md --dump --repo foo/bar --id 123
|
|
59
|
+
```
|
|
60
|
+
|
|
47
61
|
### 准备工作
|
|
48
62
|
|
|
49
63
|
操作 GitHub issue 是需要权限认证的,因此你需要向 Kup 提供 GitHub token,以便 Kup 调用 GitHub API。
|
|
@@ -65,6 +79,7 @@ Kup 会把 `file.md` 文件的内容更新到 `foo/bar` 仓库的编号为 `123`
|
|
|
65
79
|
---|---|---|---|---
|
|
66
80
|
`--repo` | `-r` | 字符串 | 指定 GitHub 仓库
|
|
67
81
|
`--id` | `-i` | 整数 | 指定 issue 的编号 | <ul><li>指定编号表示更新已有 issue<li>未指定编号则表示发布新 issue</ul>
|
|
82
|
+
`--dump` | `-d` | - | 激活采集模式,把 GitHub issue 保存为本地 Markdown 文件
|
|
68
83
|
`--version` | `-v` | - | 显示版本号
|
|
69
84
|
`--help` | `-h` | - | 显示帮助信息
|
|
70
85
|
|
|
@@ -90,7 +105,7 @@ Kup 会把 `file.md` 文件的内容更新到 `foo/bar` 仓库的编号为 `123`
|
|
|
90
105
|
1. 调用命令行时指定的 `--id` 参数。
|
|
91
106
|
1. Markdown 文件内的元数据的 `id` 字段。
|
|
92
107
|
|
|
93
|
-
当一个文件成功发布为 issue 后,Kup
|
|
108
|
+
当一个文件成功发布为 issue 后,Kup 会把 `id` 写回文件的元数据中(在写入前会向用户确认)。
|
|
94
109
|
|
|
95
110
|
### Issue 的标题是如何确定的?
|
|
96
111
|
|
package/bin/cli.js
CHANGED
|
@@ -14,8 +14,10 @@ if (isDebugging()) {
|
|
|
14
14
|
const argv = yargs(hideBin(process.argv))
|
|
15
15
|
.scriptName('kup')
|
|
16
16
|
.usage('Kup -- A CLI tool to sync local Markdown files to GitHub issues.')
|
|
17
|
-
.usage('Usage:
|
|
17
|
+
.usage('Usage (publish to GitHub): $0 <file> [options]')
|
|
18
|
+
.usage('Usage (dump to local file): $0 [file] --dump [options]')
|
|
18
19
|
.example('kup foo.md --repo aaa/bbb --id 123', '// sync foo.md to GitHub issue aaa/bbb#123')
|
|
20
|
+
.example('kup 123.md --dump --repo aaa/bbb --id 123', '// dump GitHub issue aaa/bbb#123 to 123.md')
|
|
19
21
|
.option('repo', {
|
|
20
22
|
alias: 'r',
|
|
21
23
|
type: 'string',
|
|
@@ -26,6 +28,11 @@ const argv = yargs(hideBin(process.argv))
|
|
|
26
28
|
type: 'number',
|
|
27
29
|
description: 'Specify GitHub issue ID',
|
|
28
30
|
})
|
|
31
|
+
.option('dump', {
|
|
32
|
+
alias: 'd',
|
|
33
|
+
type: 'boolean',
|
|
34
|
+
description: 'Dump a GitHub issue to a local Markdown file',
|
|
35
|
+
})
|
|
29
36
|
.option('parse-only', {
|
|
30
37
|
alias: 'p',
|
|
31
38
|
type: 'boolean',
|
package/lib/file.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import fsPromises from 'fs/promises'
|
|
2
2
|
|
|
3
|
+
async function fileExists(pathname) {
|
|
4
|
+
try {
|
|
5
|
+
await fsPromises.access(pathname)
|
|
6
|
+
return true
|
|
7
|
+
} catch {
|
|
8
|
+
return false
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
function readTextFile(pathname) {
|
|
4
13
|
return fsPromises.readFile(pathname)
|
|
5
14
|
.then((bin) => {
|
|
@@ -16,6 +25,7 @@ function writeTextFile(pathname, content) {
|
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
export {
|
|
28
|
+
fileExists,
|
|
19
29
|
readTextFile,
|
|
20
30
|
writeTextFile,
|
|
21
31
|
}
|
package/lib/main.js
CHANGED
|
@@ -1,38 +1,35 @@
|
|
|
1
|
+
import path from 'path'
|
|
1
2
|
import inquirer from 'inquirer'
|
|
2
3
|
|
|
3
4
|
import { isDebugging } from './util.js'
|
|
4
5
|
import { KupError, errorLine } from './error.js'
|
|
5
|
-
import { readTextFile } from './file.js'
|
|
6
|
+
import { fileExists, readTextFile } from './file.js'
|
|
6
7
|
import { parse } from './parse.js'
|
|
7
8
|
import { getRepo } from './repo.js'
|
|
8
|
-
import { updateIssue, postIssue } from './sync.js'
|
|
9
|
+
import { dumpIssue, updateIssue, postIssue } from './sync.js'
|
|
9
10
|
import { getToken } from './token.js'
|
|
10
11
|
|
|
11
12
|
async function main(argv) {
|
|
12
13
|
const {
|
|
14
|
+
dump = false,
|
|
13
15
|
id = 0,
|
|
16
|
+
parseOnly = false,
|
|
14
17
|
repo = '',
|
|
15
18
|
_: files,
|
|
16
19
|
} = argv
|
|
17
20
|
|
|
18
21
|
// 目前只处理第一个文件
|
|
19
|
-
const file = files[0]
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
errorLine(`[Kup] [Error] Cannot read file "${ file }"!`),
|
|
26
|
-
errorLine(e.message),
|
|
27
|
-
])
|
|
28
|
-
}
|
|
22
|
+
const file = files[0] || ''
|
|
23
|
+
const fileInfo = await getFileInfoForMode(file, {
|
|
24
|
+
dump,
|
|
25
|
+
parseOnly,
|
|
26
|
+
needsFileMeta: !repo || !id,
|
|
27
|
+
})
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (argv.parseOnly || isDebugging()) {
|
|
29
|
+
if (parseOnly || isDebugging()) {
|
|
33
30
|
console.log('[Kup] [Debug] fileInfo =', fileInfo)
|
|
34
31
|
// 如果有 p 参数,则提前退出,不需要走后面的步骤
|
|
35
|
-
if (
|
|
32
|
+
if (parseOnly) return
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
// 通过各种方式获取 repo
|
|
@@ -77,9 +74,19 @@ async function main(argv) {
|
|
|
77
74
|
|
|
78
75
|
// 检查 TOKEN
|
|
79
76
|
if (await getToken()) {
|
|
80
|
-
// 调用 GitHub API
|
|
81
77
|
const idReal = id || fileInfo.meta.id
|
|
82
|
-
if (
|
|
78
|
+
if (dump) {
|
|
79
|
+
if (!idReal) {
|
|
80
|
+
throw new KupError([
|
|
81
|
+
errorLine('[Kup] [Error] Cannot get `id` to dump!'),
|
|
82
|
+
])
|
|
83
|
+
}
|
|
84
|
+
const outputFile = file || path.resolve(`${ idReal }.md`)
|
|
85
|
+
await dumpIssue(repoReal, idReal, {
|
|
86
|
+
file: outputFile,
|
|
87
|
+
repoSource,
|
|
88
|
+
})
|
|
89
|
+
} else if (idReal) {
|
|
83
90
|
await updateIssue(fileInfo, repoReal, idReal)
|
|
84
91
|
} else {
|
|
85
92
|
await postIssue(fileInfo, repoReal, {
|
|
@@ -91,6 +98,39 @@ async function main(argv) {
|
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
100
|
|
|
101
|
+
async function getFileInfoForMode(file, {
|
|
102
|
+
dump = false,
|
|
103
|
+
parseOnly = false,
|
|
104
|
+
needsFileMeta = false,
|
|
105
|
+
} = {}) {
|
|
106
|
+
if (!dump || parseOnly) {
|
|
107
|
+
const content = await readMarkdownFile(file)
|
|
108
|
+
return parse(content)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!needsFileMeta || !file || !await fileExists(file)) {
|
|
112
|
+
return {
|
|
113
|
+
meta: {},
|
|
114
|
+
title: '',
|
|
115
|
+
content: '',
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const content = await readMarkdownFile(file)
|
|
120
|
+
return parse(content)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function readMarkdownFile(file) {
|
|
124
|
+
try {
|
|
125
|
+
return await readTextFile(file)
|
|
126
|
+
} catch (e) {
|
|
127
|
+
throw new KupError([
|
|
128
|
+
errorLine(`[Kup] [Error] Cannot read file "${ file }"!`),
|
|
129
|
+
errorLine(e.message),
|
|
130
|
+
])
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
94
134
|
function getRepoSourceLabel(source = '') {
|
|
95
135
|
switch (source) {
|
|
96
136
|
case 'package.repository':
|
package/lib/meta.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import YAML from 'yaml'
|
|
2
|
+
|
|
1
3
|
const SEPARATOR = '---'
|
|
2
4
|
const SEPARATOR_END_ALT = '...'
|
|
3
5
|
|
|
@@ -5,6 +7,8 @@ function updateIssueMeta(content, {
|
|
|
5
7
|
id,
|
|
6
8
|
repo = '',
|
|
7
9
|
shouldWriteRepo = false,
|
|
10
|
+
tags,
|
|
11
|
+
title,
|
|
8
12
|
} = {}) {
|
|
9
13
|
const eol = detectEol(content)
|
|
10
14
|
const lines = content.split(eol)
|
|
@@ -16,12 +20,16 @@ function updateIssueMeta(content, {
|
|
|
16
20
|
id,
|
|
17
21
|
repo,
|
|
18
22
|
shouldWriteRepo,
|
|
23
|
+
tags,
|
|
24
|
+
title,
|
|
19
25
|
})
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
const metaLines = lines.slice(metaSection.start + 1, metaSection.end)
|
|
23
29
|
upsertMetaLine(metaLines, 'repo', repo, shouldWriteRepo, ['id', 'tags', 'title'])
|
|
24
|
-
upsertMetaLine(metaLines, 'id',
|
|
30
|
+
upsertMetaLine(metaLines, 'id', id, typeof id !== 'undefined', ['tags', 'title'], ['repo'])
|
|
31
|
+
upsertMetaLine(metaLines, 'tags', tags, Array.isArray(tags) && tags.length > 0, ['title'], ['id', 'repo'])
|
|
32
|
+
upsertMetaLine(metaLines, 'title', title, typeof title === 'string' && !!title, [], ['tags', 'id', 'repo'])
|
|
25
33
|
|
|
26
34
|
return [
|
|
27
35
|
...lines.slice(0, metaSection.start + 1),
|
|
@@ -62,16 +70,30 @@ function detectMetaSection(lines) {
|
|
|
62
70
|
return null
|
|
63
71
|
}
|
|
64
72
|
|
|
65
|
-
function insertNewMetaSection(lines, {
|
|
73
|
+
function insertNewMetaSection(lines, {
|
|
74
|
+
eol,
|
|
75
|
+
id,
|
|
76
|
+
repo,
|
|
77
|
+
tags,
|
|
78
|
+
title,
|
|
79
|
+
shouldWriteRepo,
|
|
80
|
+
}) {
|
|
66
81
|
let insertAt = 0
|
|
67
82
|
while (insertAt < lines.length && !lines[insertAt].trim()) {
|
|
68
83
|
insertAt++
|
|
69
84
|
}
|
|
70
85
|
|
|
71
|
-
const metaLines = [
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
const metaLines = [
|
|
87
|
+
SEPARATOR,
|
|
88
|
+
...buildMetaFields({
|
|
89
|
+
id,
|
|
90
|
+
repo,
|
|
91
|
+
tags,
|
|
92
|
+
title,
|
|
93
|
+
shouldWriteRepo,
|
|
94
|
+
}),
|
|
95
|
+
SEPARATOR,
|
|
96
|
+
]
|
|
75
97
|
|
|
76
98
|
const bodyLines = lines.slice(insertAt)
|
|
77
99
|
const result = [...metaLines]
|
|
@@ -93,7 +115,7 @@ function upsertMetaLine(metaLines, key, value, shouldWrite, beforeKeys = [], aft
|
|
|
93
115
|
}
|
|
94
116
|
|
|
95
117
|
const insertIndex = findInsertIndex(metaLines, beforeKeys, afterKeys)
|
|
96
|
-
metaLines.splice(insertIndex, 0,
|
|
118
|
+
metaLines.splice(insertIndex, 0, stringifyMetaField(key, value))
|
|
97
119
|
}
|
|
98
120
|
|
|
99
121
|
function findMetaLineIndex(metaLines, key) {
|
|
@@ -105,10 +127,10 @@ function findMetaLineIndex(metaLines, key) {
|
|
|
105
127
|
|
|
106
128
|
function replaceMetaLineValue(line, key, value) {
|
|
107
129
|
const match = new RegExp(`^(\\s*${ key }\\s*:)(\\s*)(.*?)(\\s+#.*)?$`).exec(line)
|
|
108
|
-
if (!match) return
|
|
130
|
+
if (!match) return stringifyMetaField(key, value)
|
|
109
131
|
|
|
110
132
|
const [, prefix, , , comment = ''] = match
|
|
111
|
-
return `${ prefix } ${ value }${ comment }`
|
|
133
|
+
return `${ prefix } ${ stringifyMetaValue(value, { flow: Array.isArray(value) }) }${ comment }`
|
|
112
134
|
}
|
|
113
135
|
|
|
114
136
|
function findInsertIndex(metaLines, beforeKeys, afterKeys) {
|
|
@@ -126,6 +148,61 @@ function findInsertIndex(metaLines, beforeKeys, afterKeys) {
|
|
|
126
148
|
return afterIndex >= 0 ? afterIndex + 1 : metaLines.length
|
|
127
149
|
}
|
|
128
150
|
|
|
151
|
+
function buildIssueMarkdown({
|
|
152
|
+
body = '',
|
|
153
|
+
id,
|
|
154
|
+
repo = '',
|
|
155
|
+
tags,
|
|
156
|
+
title,
|
|
157
|
+
shouldWriteRepo = false,
|
|
158
|
+
} = {}) {
|
|
159
|
+
const metaLines = [
|
|
160
|
+
SEPARATOR,
|
|
161
|
+
...buildMetaFields({
|
|
162
|
+
id,
|
|
163
|
+
repo,
|
|
164
|
+
tags,
|
|
165
|
+
title,
|
|
166
|
+
shouldWriteRepo,
|
|
167
|
+
}),
|
|
168
|
+
SEPARATOR,
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
return `${ metaLines.join('\n') }\n\n${ body || '' }`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function buildMetaFields({
|
|
175
|
+
id,
|
|
176
|
+
repo = '',
|
|
177
|
+
tags,
|
|
178
|
+
title,
|
|
179
|
+
shouldWriteRepo = false,
|
|
180
|
+
} = {}) {
|
|
181
|
+
const fields = []
|
|
182
|
+
if (shouldWriteRepo) fields.push(stringifyMetaField('repo', repo))
|
|
183
|
+
if (typeof id !== 'undefined') fields.push(stringifyMetaField('id', id))
|
|
184
|
+
if (Array.isArray(tags) && tags.length > 0) fields.push(stringifyMetaField('tags', tags))
|
|
185
|
+
if (typeof title === 'string' && title) fields.push(stringifyMetaField('title', title))
|
|
186
|
+
return fields
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function stringifyMetaField(key, value) {
|
|
190
|
+
return `${ key }: ${ stringifyMetaValue(value, { flow: Array.isArray(value) }) }`
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function stringifyMetaValue(value, { flow = false } = {}) {
|
|
194
|
+
const doc = new YAML.Document()
|
|
195
|
+
doc.contents = doc.createNode({ value })
|
|
196
|
+
const node = doc.get('value', true)
|
|
197
|
+
if (flow && node) node.flow = true
|
|
198
|
+
const output = doc.toString({
|
|
199
|
+
directives: false,
|
|
200
|
+
flowCollectionPadding: false,
|
|
201
|
+
}).trim()
|
|
202
|
+
return output.replace(/^value:\s*/, '')
|
|
203
|
+
}
|
|
204
|
+
|
|
129
205
|
export {
|
|
206
|
+
buildIssueMarkdown,
|
|
130
207
|
updateIssueMeta,
|
|
131
208
|
}
|
package/lib/parse.js
CHANGED
|
@@ -39,9 +39,11 @@ function parse(text) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function _detectMetaSectionLineQty(lines) {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
42
|
+
const firstLine = lines[0] || ''
|
|
43
|
+
const secondLine = lines[1] || ''
|
|
44
|
+
const hasSeparatorStart = firstLine.trimEnd() === SEPARATOR
|
|
45
|
+
const noEmptyLineAfterSeparatorStart = !!secondLine.trimEnd()
|
|
46
|
+
const hasYamlKey = /^#?\s*[\w\-]+:/.test(secondLine.trim())
|
|
45
47
|
|
|
46
48
|
let metaLineQty = 0
|
|
47
49
|
if (hasSeparatorStart && noEmptyLineAfterSeparatorStart && hasYamlKey) {
|
|
@@ -111,7 +113,7 @@ function _stripMetaSection(lines, metaLineQty) {
|
|
|
111
113
|
if (emptyLineQty) lines.splice(0, emptyLineQty)
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
function _getTitleFromMainBody(firstLine) {
|
|
116
|
+
function _getTitleFromMainBody(firstLine = '') {
|
|
115
117
|
const re = /^#([^#].*)/
|
|
116
118
|
const result = re.exec(firstLine.trim())
|
|
117
119
|
const title = result ? result[1].trim() : ''
|
package/lib/sync.js
CHANGED
|
@@ -2,9 +2,9 @@ import ghGot from 'gh-got'
|
|
|
2
2
|
import inquirer from 'inquirer'
|
|
3
3
|
|
|
4
4
|
import { KupError, errorLine, logLine } from './error.js'
|
|
5
|
-
import { readTextFile, writeTextFile } from './file.js'
|
|
6
|
-
import { updateIssueMeta } from './meta.js'
|
|
7
|
-
import { isDebugging } from '
|
|
5
|
+
import { fileExists, readTextFile, writeTextFile } from './file.js'
|
|
6
|
+
import { buildIssueMarkdown, updateIssueMeta } from './meta.js'
|
|
7
|
+
import { isDebugging } from './util.js'
|
|
8
8
|
|
|
9
9
|
let proxyOptionsForGot = {}
|
|
10
10
|
if (isDebugging()) {
|
|
@@ -100,16 +100,101 @@ async function postIssue(fileInfo, repo, options = {}) {
|
|
|
100
100
|
console.log(`[Kup] [Success] URL: ${ url }`)
|
|
101
101
|
|
|
102
102
|
if (options.file) {
|
|
103
|
-
await
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
const shouldWriteMeta = await confirmWriteIssueMeta(options.file)
|
|
104
|
+
if (shouldWriteMeta) {
|
|
105
|
+
await writeIssueMeta(options.file, {
|
|
106
|
+
id,
|
|
107
|
+
repo,
|
|
108
|
+
shouldWriteRepo: options.repoSource === 'cli' && !options.hasRepoInMeta,
|
|
109
|
+
})
|
|
110
|
+
console.log(`[Kup] [Notice] Updated metadata in Markdown file: ${ options.file }`)
|
|
111
|
+
}
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
async function dumpIssue(repo, id, options = {}) {
|
|
117
|
+
const api = `/repos/${ repo }/issues/${ id }`
|
|
118
|
+
const url = `https://github.com/${ repo }/issues/${ id }`
|
|
119
|
+
let response = null
|
|
120
|
+
console.log(`[Kup] Dumping "${ repo }#${ id }"...`)
|
|
121
|
+
console.log(`[Kup] Dumping URL: ${ url }`)
|
|
122
|
+
try {
|
|
123
|
+
response = await ghGot(api, {
|
|
124
|
+
token: process.env.GITHUB_TOKEN,
|
|
125
|
+
...proxyOptionsForGot,
|
|
126
|
+
})
|
|
127
|
+
} catch (e) {
|
|
128
|
+
throw new KupError([
|
|
129
|
+
errorLine('[Kup] [Error] Request error: ' + e.message),
|
|
130
|
+
])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!response) return
|
|
134
|
+
|
|
135
|
+
const outputFile = options.file
|
|
136
|
+
if (outputFile && await fileExists(outputFile)) {
|
|
137
|
+
const shouldOverwrite = await confirmOverwriteDumpFile(outputFile)
|
|
138
|
+
if (!shouldOverwrite) {
|
|
139
|
+
throw new KupError([
|
|
140
|
+
logLine('[Kup] Aborted!'),
|
|
141
|
+
])
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const issue = response.body || {}
|
|
146
|
+
const title = issue.title || ''
|
|
147
|
+
const body = issue.body || ''
|
|
148
|
+
const tags = Array.isArray(issue.labels)
|
|
149
|
+
? issue.labels
|
|
150
|
+
.map((label) => String(label?.name || '').trim())
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
: []
|
|
153
|
+
const content = buildIssueMarkdown({
|
|
154
|
+
body,
|
|
155
|
+
id,
|
|
156
|
+
repo,
|
|
157
|
+
shouldWriteRepo: options.repoSource === 'cli',
|
|
158
|
+
tags,
|
|
159
|
+
title,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
await writeTextFile(outputFile, content)
|
|
164
|
+
} catch (e) {
|
|
165
|
+
throw new KupError([
|
|
166
|
+
errorLine(`[Kup] [Error] Cannot write file "${ outputFile }"!`),
|
|
167
|
+
errorLine(e.message),
|
|
168
|
+
])
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(`[Kup] [Success] Dumped "${ repo }#${ id }" to "${ outputFile }"!`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function confirmWriteIssueMeta(file) {
|
|
175
|
+
const answer = await inquirer.prompt([
|
|
176
|
+
{
|
|
177
|
+
name: 'writeIssueMeta',
|
|
178
|
+
type: 'confirm',
|
|
179
|
+
message: `Kup is going to write the new issue metadata back to "${ file }", OK?`,
|
|
180
|
+
default: true,
|
|
181
|
+
},
|
|
182
|
+
])
|
|
183
|
+
return answer.writeIssueMeta
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function confirmOverwriteDumpFile(file) {
|
|
187
|
+
const answer = await inquirer.prompt([
|
|
188
|
+
{
|
|
189
|
+
name: 'overwriteDumpFile',
|
|
190
|
+
type: 'confirm',
|
|
191
|
+
message: `Kup is going to overwrite the local file "${ file }", OK?`,
|
|
192
|
+
default: true,
|
|
193
|
+
},
|
|
194
|
+
])
|
|
195
|
+
return answer.overwriteDumpFile
|
|
196
|
+
}
|
|
197
|
+
|
|
113
198
|
async function writeIssueMeta(file, { id, repo, shouldWriteRepo }) {
|
|
114
199
|
let content = ''
|
|
115
200
|
try {
|
|
@@ -159,6 +244,9 @@ function buildPostIssuePayload(fileInfo) {
|
|
|
159
244
|
export {
|
|
160
245
|
buildPostIssuePayload,
|
|
161
246
|
buildUpdateIssuePayload,
|
|
247
|
+
confirmOverwriteDumpFile,
|
|
248
|
+
confirmWriteIssueMeta,
|
|
249
|
+
dumpIssue,
|
|
162
250
|
updateIssue,
|
|
163
251
|
postIssue,
|
|
164
252
|
writeIssueMeta,
|
package/lib/validate.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
function validate(argv) {
|
|
2
2
|
const {
|
|
3
|
+
dump = false,
|
|
3
4
|
id,
|
|
4
5
|
repo,
|
|
5
6
|
_: files,
|
|
@@ -8,7 +9,8 @@ function validate(argv) {
|
|
|
8
9
|
let results = []
|
|
9
10
|
if ('id' in argv) results.push(validateId(id))
|
|
10
11
|
if ('repo' in argv) results.push(validateRepo(repo))
|
|
11
|
-
results.push(validateFiles(files))
|
|
12
|
+
results.push(validateFiles(files, { required: !dump }))
|
|
13
|
+
if (dump) results.push(validateDumpArgs(argv))
|
|
12
14
|
// 筛选出有错误的结果
|
|
13
15
|
const errorResults = results.filter((result) => result.status === false)
|
|
14
16
|
|
|
@@ -48,9 +50,9 @@ function validateRepo(repo) {
|
|
|
48
50
|
errorMsg,
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
|
-
function validateFiles(files) {
|
|
53
|
+
function validateFiles(files, { required = true } = {}) {
|
|
52
54
|
let errorMsg = ''
|
|
53
|
-
if (!files.length) {
|
|
55
|
+
if (required && !files.length) {
|
|
54
56
|
errorMsg = 'Must specify a local file!'
|
|
55
57
|
} else if (files.some((item) => !item)) {
|
|
56
58
|
errorMsg = 'File name must not be empty!'
|
|
@@ -61,8 +63,25 @@ function validateFiles(files) {
|
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
function validateDumpArgs(argv) {
|
|
67
|
+
const {
|
|
68
|
+
_: files,
|
|
69
|
+
} = argv
|
|
70
|
+
|
|
71
|
+
let errorMsg = ''
|
|
72
|
+
if (!files.length && !('id' in argv)) {
|
|
73
|
+
errorMsg = 'Must specify a local file or `--id` in dump mode!'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
status: !errorMsg,
|
|
78
|
+
errorMsg,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
64
82
|
export {
|
|
65
83
|
validate,
|
|
84
|
+
validateDumpArgs,
|
|
66
85
|
validateId,
|
|
67
86
|
validateRepo,
|
|
68
87
|
validateFiles,
|