kup-cli 0.1.4 → 0.2.1

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 CHANGED
@@ -4,26 +4,28 @@ English | [中文](README.zh.md)
4
4
 
5
5
  <img src="https://user-images.githubusercontent.com/1231359/171321963-e8e73bdf-f9c8-435b-9707-318e92f1805a.png" width="150" height="80" align="right" alt="Kup logo">
6
6
 
7
- > A CLI tool to sync local Markdown files to GitHub issues.
7
+ > A CLI tool to sync local Markdown files and GitHub issues bidirectionally.
8
8
  >
9
- > 这款命令行工具可以把本地的 Markdown 文件同步到 GitHub issue。
9
+ > 一款命令行工具,实现本地 Markdown 文件与 GitHub Issues 的双向同步。
10
+
11
+
10
12
 
11
13
  ## Who Is It For
12
14
 
13
- * Bloggers who publish with GitHub issues and want to edit drafts locally, then publish new posts or update existing ones at any time.
14
- * Open-source authors who publish documentation through GitHub issues and want to sync docs from the repository to specific issues.
15
+ * Bloggers who publish with GitHub issues and want to edit drafts locally.
16
+ * Open-source authors who publish documentation through GitHub issues.
15
17
  * Anyone else who needs to update GitHub issues frequently.
16
18
 
17
19
 
18
- ## Installation
19
20
 
20
- Install it globally so you can call it from the command line at any time:
21
+ ## Installation
21
22
 
22
23
  ```sh
23
24
  npm install -g kup-cli
24
25
  ```
25
26
 
26
- (When a new version of Kup is released, run the same command again to upgrade.)
27
+ Install it globally so you can use it anytime, anywhere from the command line. (When a new version of Kup is released, run the same command again to upgrade.)
28
+
27
29
 
28
30
 
29
31
  ## Usage
@@ -44,6 +46,20 @@ kup ./path/to/file.md --repo foo/bar --id 123
44
46
 
45
47
  Kup will update issue `123` in the `foo/bar` repository with the content of `file.md`.
46
48
 
49
+ ### Dump an Existing Issue
50
+
51
+ ```sh
52
+ kup --dump --repo foo/bar --id 123
53
+ ```
54
+
55
+ Kup will dump issue `123` from the `foo/bar` repository to `123.md` in the current directory.
56
+
57
+ If you want to specify the output file, you can also use:
58
+
59
+ ```sh
60
+ kup ./path/to/file.md --dump --repo foo/bar --id 123
61
+ ```
62
+
47
63
  ### Preparation
48
64
 
49
65
  Updating GitHub issues requires authentication, so you need to provide a GitHub token for Kup to call the GitHub API.
@@ -59,16 +75,35 @@ Updating GitHub issues requires authentication, so you need to provide a GitHub
59
75
  If Kup cannot read a token from the environment, it will prompt you for one in the terminal.
60
76
 
61
77
 
78
+
62
79
  ## Command-Line Options
63
80
 
64
- Option | Short | Value Type | Description | Notes
65
- ---|---|---|---|---
81
+ Option | Short | Value Type | Description
82
+ ---|---|---|---
66
83
  `--repo` | `-r` | string | Specify the GitHub repository
67
- `--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>
84
+ `--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>
85
+ `--dump` | `-d` | - | Enable dump mode, save a GitHub issue as a local Markdown file
68
86
  `--version` | `-v` | - | Show the version number
69
87
  `--help` | `-h` | - | Show help information
70
88
 
71
89
 
90
+
91
+ &nbsp;
92
+
93
+ > ***
94
+ >
95
+ > ### **🙋‍♂️ Open to Work (Remote, Global)**
96
+ >
97
+ > * Senior Fullstack Engineer · Node.js, React, Vue, TS · 10+ years
98
+ > * Published author on “AI Coding” · GitHub 3k+ followers
99
+ > * → Resume / Contact: https://github.com/cssmagic/cssmagic
100
+ >
101
+ > ***
102
+
103
+ &nbsp;
104
+
105
+
106
+
72
107
  ## Advanced
73
108
 
74
109
  ### How can I specify the `repo` option more conveniently?
@@ -90,7 +125,7 @@ Kup can determine the `id` option in the following order of priority:
90
125
  1. The `--id` option passed on the command line.
91
126
  1. The `id` field in the Markdown file's metadata.
92
127
 
93
- After a file is successfully published as an issue, Kup immediately writes the `id` back into its metadata.
128
+ 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
129
 
95
130
  ### How is the issue title determined?
96
131
 
@@ -117,12 +152,15 @@ What happens if there is no `tags` field in the metadata?
117
152
 
118
153
  When updating an existing issue, if the labels specified in the metadata differ from the issue's current labels, the new set completely replaces the old one.
119
154
 
155
+
156
+
120
157
  ## Documentation
121
158
 
122
159
  * [Metadata examples for Markdown files](https://github.com/cssmagic/kup/issues/1)
123
160
  * [How to generate an appropriate GitHub token for Kup](https://github.com/cssmagic/kup/issues/17)
124
161
 
125
162
 
163
+
126
164
  ## Other
127
165
 
128
166
  ### Roadmap
package/README.zh.md CHANGED
@@ -4,9 +4,11 @@
4
4
 
5
5
  <img src="https://user-images.githubusercontent.com/1231359/171321963-e8e73bdf-f9c8-435b-9707-318e92f1805a.png" width="150" height="80" align="right" alt="Kup logo">
6
6
 
7
- > A CLI tool to sync local Markdown files to GitHub issues.
7
+ > A CLI tool to sync local Markdown files and GitHub issues bidirectionally.
8
8
  >
9
- > 这款命令行工具可以把本地的 Markdown 文件同步到 GitHub issue。
9
+ > 一款命令行工具,实现本地 Markdown 文件与 GitHub Issues 的双向同步。
10
+
11
+
10
12
 
11
13
  ## 谁需要它
12
14
 
@@ -15,15 +17,15 @@
15
17
  * 其它需要频繁更新 GitHub issue 的人。
16
18
 
17
19
 
18
- ## 安装
19
20
 
20
- 需要全局安装,以便随时在命令行调用:
21
+ ## 安装
21
22
 
22
23
  ```sh
23
24
  npm install -g kup-cli
24
25
  ```
25
26
 
26
- (当 Kup 发布新版时,可以再次运行这行命令升级已安装的版本。)
27
+ 需要全局安装,以便随时在命令行调用。(当 Kup 发布新版时,可以再次运行这行命令升级已安装的版本。)
28
+
27
29
 
28
30
 
29
31
  ## 使用方法
@@ -44,6 +46,20 @@ kup ./path/to/file.md --repo foo/bar --id 123
44
46
 
45
47
  Kup 会把 `file.md` 文件的内容更新到 `foo/bar` 仓库的编号为 `123` 的 issue。
46
48
 
49
+ ### 采集已有 issue
50
+
51
+ ```sh
52
+ kup --dump --repo foo/bar --id 123
53
+ ```
54
+
55
+ Kup 会把 `foo/bar` 仓库的编号为 `123` 的 issue 采集为当前目录下的 `123.md` 文件。
56
+
57
+ 如果你想指定输出文件,也可以这样使用:
58
+
59
+ ```sh
60
+ kup ./path/to/file.md --dump --repo foo/bar --id 123
61
+ ```
62
+
47
63
  ### 准备工作
48
64
 
49
65
  操作 GitHub issue 是需要权限认证的,因此你需要向 Kup 提供 GitHub token,以便 Kup 调用 GitHub API。
@@ -59,16 +75,19 @@ Kup 会把 `file.md` 文件的内容更新到 `foo/bar` 仓库的编号为 `123`
59
75
  如果 Kup 未能从环境变量获取 token,会在命令行向你询问。
60
76
 
61
77
 
78
+
62
79
  ## 命令行参数
63
80
 
64
- 参数 | 短名 | 值类型 | 含义 | 备注
65
- ---|---|---|---|---
81
+ 参数 | 短名 | 值类型 | 含义
82
+ ---|---|---|---
66
83
  `--repo` | `-r` | 字符串 | 指定 GitHub 仓库
67
- `--id` | `-i` | 整数 | 指定 issue 的编号 | <ul><li>指定编号表示更新已有 issue<li>未指定编号则表示发布新 issue</ul>
84
+ `--id` | `-i` | 整数 | 指定 issue 的编号: <ul><li>指定编号表示更新已有 issue<li>未指定编号则表示发布新 issue</ul>
85
+ `--dump` | `-d` | - | 激活采集模式,把 GitHub issue 保存为本地 Markdown 文件
68
86
  `--version` | `-v` | - | 显示版本号
69
87
  `--help` | `-h` | - | 显示帮助信息
70
88
 
71
89
 
90
+
72
91
  ## 进阶
73
92
 
74
93
  ### 如何更方便地指定 `repo` 参数?
@@ -90,7 +109,7 @@ Kup 会把 `file.md` 文件的内容更新到 `foo/bar` 仓库的编号为 `123`
90
109
  1. 调用命令行时指定的 `--id` 参数。
91
110
  1. Markdown 文件内的元数据的 `id` 字段。
92
111
 
93
- 当一个文件成功发布为 issue 后,Kup 会立即把 `id` 写入它的元数据中。
112
+ 当一个文件成功发布为 issue 后,Kup 会把 `id` 写回文件的元数据中(在写入前会向用户确认)。
94
113
 
95
114
  ### Issue 的标题是如何确定的?
96
115
 
@@ -117,12 +136,15 @@ Kup 会通过以下线索来确定 issue 的标题,优先级递减:
117
136
 
118
137
  在更新已有 issue 时,如果元数据指定的标签与 issue 现有标签不一致,则前者会完全替代后者。
119
138
 
139
+
140
+
120
141
  ## 文档
121
142
 
122
143
  * [Markdown 文件内的元数据示例](https://github.com/cssmagic/kup/issues/1)
123
144
  * [如何为 Kup 生成合适的 GitHub token](https://github.com/cssmagic/kup/issues/17)
124
145
 
125
146
 
147
+
126
148
  ## 其它
127
149
 
128
150
  ### 开发计划
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: $0 <file> [options]')
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
- let content = ''
21
- try {
22
- content = await readTextFile(file)
23
- } catch (e) {
24
- throw new KupError([
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
- const fileInfo = parse(content)
31
-
32
- if (argv.parseOnly || isDebugging()) {
29
+ if (parseOnly || isDebugging()) {
33
30
  console.log('[Kup] [Debug] fileInfo =', fileInfo)
34
31
  // 如果有 p 参数,则提前退出,不需要走后面的步骤
35
- if (argv.parseOnly) return
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 (idReal) {
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', String(id), true, ['tags', 'title'], ['repo'])
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, { eol, id, repo, shouldWriteRepo }) {
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 = [SEPARATOR]
72
- if (shouldWriteRepo) metaLines.push(`repo: ${ repo }`)
73
- metaLines.push(`id: ${ id }`)
74
- metaLines.push(SEPARATOR)
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, `${ key }: ${ value }`)
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 `${ key }: ${ value }`
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 hasSeparatorStart = lines[0].trimEnd() === SEPARATOR
43
- const noEmptyLineAfterSeparatorStart = !!lines[1].trimEnd()
44
- const hasYamlKey = /^#?\s*[\w\-]+:/.test(lines[1].trim())
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 '../lib/util.js'
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 writeIssueMeta(options.file, {
104
- id,
105
- repo,
106
- shouldWriteRepo: options.repoSource === 'cli' && !options.hasRepoInMeta,
107
- })
108
- console.log(`[Kup] [Notice] Updated metadata in Markdown file: ${ options.file }`)
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kup-cli",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "A CLI tool to sync local Markdown files to GitHub issues.",
5
5
  "type": "module",
6
6
  "bin": {