kup-cli 0.0.0-alpha.4 → 0.1.0-beta.3

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/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2022 cssmagic and other contributors
3
+ Copyright (c) 2022 cssmagic
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,9 +1,145 @@
1
1
  # Kup
2
2
 
3
- > A CLI tool to post Markdown files to GitHub as issues.
3
+ <img src="https://user-images.githubusercontent.com/1231359/171321963-e8e73bdf-f9c8-435b-9707-318e92f1805a.png" width="150" height="80" align="right" alt="Kup logo">
4
4
 
5
- ## Installation
5
+ > A CLI tool to sync local Markdown files to GitHub issues.
6
+ >
7
+ > 这款命令行工具可以把本地的 Markdown 文件同步到 GitHub issue。
8
+
9
+ ## 谁需要它
10
+
11
+ * 利用 GitHub issue 写博客的作者们,可以方便地在本地编辑原稿,并随时发布新博文或更新已有博文。
12
+ * 通过 GitHub issue 发布文档的开源软件作者们,可以把代码仓库中的文档更新到指定 issue。
13
+ * 其它需要频繁更新 GitHub issue 的人。
14
+
15
+
16
+ ## 安装
17
+
18
+ 需要全局安装,以便随时在命令行调用:
6
19
 
7
20
  ```sh
8
21
  npm i -g kup-cli
9
22
  ```
23
+
24
+ (当 Kup 发布新版时,可以再次运行这行命令升级已安装的版本。)
25
+
26
+
27
+ ## 使用方法
28
+
29
+ ### 发布新 issue
30
+
31
+ ```sh
32
+ kup ./path/to/file.md --repo foo/bar
33
+ ```
34
+
35
+ Kup 会把 `file.md` 文件的内容发布为 `foo/bar` 仓库的一个新 issue,发布成功后会告知新 issue 的编号。
36
+
37
+ ### 更新已有 issue
38
+
39
+ ```sh
40
+ kup ./path/to/file.md --repo foo/bar --id 123
41
+ ```
42
+
43
+ Kup 会把 `file.md` 文件的内容更新到 `foo/bar` 仓库的编号为 `123` 的 issue。
44
+
45
+ ### 准备工作
46
+
47
+ 操作 GitHub issue 是需要权限认证的,因此你需要向 Kup 提供 GitHub token,以便 Kup 调用 GitHub API。
48
+
49
+ 1. 在 GitHub 的 “[Personal access tokens](https://github.com/settings/tokens)” 页面生成一个新 token,权限范围需要选中 “repo”。(详细说明参见 [这篇文档](https://github.com/cssmagic/kup/issues/17)。)
50
+
51
+ 2. 把获取到的 token 写入环境变量:
52
+
53
+ ```sh
54
+ export GITHUB_TOKEN=ghp_**********
55
+ ```
56
+
57
+ 如果 Kup 未能从环境变量获取 token,会在命令行向你询问。
58
+
59
+
60
+ ## 命令行参数
61
+
62
+ 参数 | 短名 | 值类型 | 含义 | 备注
63
+ ---|---|---|---|---
64
+ `--repo` | `-r` | 字符串 | 指定 GitHub 仓库
65
+ `--id` | `-i` | 整数 | 指定 issue 的编号 | <li>指定编号表示更新已有 issue<li>未指定编号则表示发布新 issue
66
+ `--version` | `-v` | - | 显示版本号
67
+ `--help` | `-h` | - | 显示帮助信息
68
+
69
+
70
+ ## 进阶
71
+
72
+ ### 如何更方便地指定 `repo` 参数?
73
+
74
+ 有以下方式可以指定 `repo` 参数,优先级递减:
75
+
76
+ 1. 调用命令行时指定的 `--repo` 参数。
77
+ 1. Markdown 文件内的 [元数据](https://github.com/cssmagic/kup/issues/1) 的 `repo` 字段。
78
+ 1. 当前项目的 `package.json` 文件内的 `kup.repo` 字段。Kup 会从当前目录向上逐级寻找 `package.json` 文件。
79
+
80
+ 如果整个项目的同步目标都是同一个仓库,则可以采用最后一种方式统一指定 `repo` 参数。
81
+
82
+ ### 如何更方便地指定 `id` 参数?
83
+
84
+ 有以下方式可以指定 `id` 参数,优先级递减:
85
+
86
+ 1. 调用命令行时指定的 `--id` 参数。
87
+ 1. Markdown 文件内的元数据的 `id` 字段。
88
+
89
+ 当一个文件发布成功后,建议立即把 id 写入它的元数据。
90
+
91
+ ### Issue 的标题是怎么确定的?
92
+
93
+ Kup 通过以下线索来确定 issue 的标题,优先级递减:
94
+
95
+ 1. Markdown 文件内的元数据的 `title` 字段。
96
+ 1. Markdown 正文的第一个标记如果是一级标题(`# Title` 格式),则取它的内容(在这种情况下,这个一级标题在同步时会从内容中排除)。
97
+
98
+ 如果通过以上方式无法确定 issue 标题,Kup 会怎么处理?
99
+
100
+ * 如果是在发布新 issue,则 Kup 会自己生成一个标题。
101
+ * 如果是在更新已有 issue,则 Kup 会忽略标题(也就是说,不会修改已有标题)。
102
+
103
+ ### 如何为 issue 指定 label?
104
+
105
+ 不论是在写博客,还是在发表 issue,你常常都会有打标签的需求。于是 Kup 也实现了这个功能。
106
+
107
+ 你需要在 Markdown 文件的元数据中添加 `tags` 字段,指定一个或多个 label。这些 label 不需要事先在 GitHub 仓库里创建好——如果你指定了不存在的 label,会在发布 issue 时自动创建。
108
+
109
+ 如果元数据中没有 `tags` 字段,Kup 会怎么处理?
110
+
111
+ * 如果是在发布新 issue,则 Kup 不会为 issue 设置任何 label。
112
+ * 如果是在更新已有 issue 时,Kup 会忽略标签(也就是说,不会修改已有标签)。
113
+
114
+ 在更新已有 issue 时,如果元数据指定的标签与 issue 现有标签不一致,则前者会完全替代后者。
115
+
116
+ ## 文档
117
+
118
+ * [Markdown 文件内的元数据示例](https://github.com/cssmagic/kup/issues/1)
119
+ * [如何为 Kup 生成合适的 GitHub token](https://github.com/cssmagic/kup/issues/17)
120
+
121
+
122
+ ## 其它
123
+
124
+ ### 开发计划
125
+
126
+ 请参考 [本项目的 issue](https://github.com/cssmagic/kup/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc)。欢迎提出你的需求,或参与 RFC 讨论。
127
+
128
+ ### 关于名字
129
+
130
+ * Kup 取自 “皮卡” 的英文词 pickup,喻意是 “轻便地运载”。
131
+ * Kup 是变形金刚人物 “杯子” 的英文名。
132
+
133
+ ### 关于 Logo
134
+
135
+ * 作者 [Fasil](https://freeicons.io/profile/722),由 [freeicons.io](https://freeicons.io/icon/e-commerce-icons/pickup-truck-icon-26893) 免费提供。
136
+
137
+ ***
138
+
139
+ ## License
140
+
141
+ > Any code contributed to this project is considered authorized for commercial use by the project authors and their affiliated companies and distributed under this project's license.
142
+ >
143
+ > 任何贡献到本项目的代码,均视为授权本项目作者及其名下公司用于商业用途、并按本项目协议分发。
144
+
145
+ MIT
package/bin/cli.js CHANGED
@@ -1,4 +1,50 @@
1
1
  #!/usr/bin/env node
2
- 'use strict'
2
+ import yargs from 'yargs'
3
+ import { hideBin } from 'yargs/helpers'
4
+ import { isDebugging } from '../lib/util.js'
5
+ import { main } from '../lib/main.js'
6
+ import { validate } from '../lib/validate.js'
3
7
 
4
- console.log('OK')
8
+ if (isDebugging()) {
9
+ console.log('[Kup] [Debug] Debug Mode is ON!')
10
+ console.log('')
11
+ }
12
+
13
+ const argv = yargs(hideBin(process.argv))
14
+ .scriptName('kup')
15
+ .usage('Kup -- A CLI tool to sync local Markdown files to GitHub issues.')
16
+ .usage('Usage: $0 <file> [options]')
17
+ .example('kup foo.md --repo aaa/bbb --id 123', '// sync foo.md to GitHub issue aaa/bbb#123')
18
+ .option('repo', {
19
+ alias: 'r',
20
+ type: 'string',
21
+ description: 'Specify GitHub repository',
22
+ })
23
+ .option('id', {
24
+ alias: 'i',
25
+ type: 'number',
26
+ description: 'Specify GitHub issue ID',
27
+ })
28
+ .option('parse-only', {
29
+ alias: 'p',
30
+ type: 'boolean',
31
+ description: 'Only parse file, without posting or syncing',
32
+ hidden: true,
33
+ })
34
+ .alias('v', 'version')
35
+ .alias('h', 'help')
36
+ .parse()
37
+
38
+ // debug
39
+ if (isDebugging()) {
40
+ console.log('[Kup] [Debug] argv =', argv)
41
+ }
42
+
43
+ // validate args
44
+ if (!validate(argv)) process.exit(1)
45
+
46
+ // do the job
47
+ main(argv)
48
+ .then(() => {
49
+ console.log('[Kup] Done!')
50
+ })
package/index.js CHANGED
@@ -1,5 +1,3 @@
1
- 'use strict'
2
-
3
- module.exports = function () {
1
+ export default function () {
4
2
  console.warn('Warning: This package is a CLI tool, you should install it globally! See README.md for more information.')
5
3
  }
package/lib/file.js ADDED
@@ -0,0 +1,21 @@
1
+ import fsPromises from 'fs/promises'
2
+
3
+ function readTextFile(pathname) {
4
+ return fsPromises.readFile(pathname)
5
+ .then((bin) => {
6
+ // 去除 utf-8 文本文件的 BOM
7
+ if (bin[0] === 0xEF && bin[1] === 0xBB && bin[2] === 0xBF) {
8
+ bin = bin.slice(3)
9
+ }
10
+ return bin.toString('utf-8')
11
+ })
12
+ }
13
+
14
+ function writeTextFile(pathname, content) {
15
+ fsPromises.writeFile(pathname, content)
16
+ }
17
+
18
+ export {
19
+ readTextFile,
20
+ writeTextFile,
21
+ }
package/lib/main.js ADDED
@@ -0,0 +1,57 @@
1
+ import { isDebugging } from './util.js'
2
+ import { readTextFile } from './file.js'
3
+ import { parse } from './parse.js'
4
+ import { getRepo } from './repo.js'
5
+ import { updateIssue, postIssue } from './sync.js'
6
+ import { getToken } from './token.js'
7
+
8
+ async function main(argv) {
9
+ const {
10
+ id = 0,
11
+ repo = '',
12
+ _: files,
13
+ } = argv
14
+
15
+ // 目前只处理第一个文件
16
+ const file = files[0]
17
+ let content = ''
18
+ try {
19
+ content = await readTextFile(file)
20
+ } catch (e) {
21
+ console.error(`[Kup] [Error] Cannot read file "${ file }"!`)
22
+ console.error(e.message)
23
+ process.exit(1)
24
+ }
25
+
26
+ const fileInfo = parse(content)
27
+
28
+ if (argv.parseOnly || isDebugging()) {
29
+ console.log('[Kup] [Debug] fileInfo =', fileInfo)
30
+ // 如果有 p 参数,则提前退出,不需要走后面的步骤
31
+ if (argv.parseOnly) return
32
+ }
33
+
34
+ // 通过各种方式获取 repo
35
+ // 优先级: 命令行参数 > 文件元数据 > 文件所在项目的 package.json 中的 `kup.repo` 字段
36
+ let repoReal = repo || fileInfo.meta.repo
37
+ if (!repoReal) repoReal = await getRepo(file)
38
+ if (!repoReal) {
39
+ console.error('[Kup] [Error] Cannot get `repo` to sync to!')
40
+ process.exit(1)
41
+ }
42
+
43
+ // 检查 TOKEN
44
+ if (await getToken()) {
45
+ // 调用 GitHub API
46
+ const idReal = id || fileInfo.meta.id
47
+ if (idReal) {
48
+ await updateIssue(fileInfo, repoReal, idReal)
49
+ } else {
50
+ await postIssue(fileInfo, repoReal)
51
+ }
52
+ }
53
+ }
54
+
55
+ export {
56
+ main,
57
+ }
package/lib/parse.js CHANGED
@@ -1,7 +1,120 @@
1
- 'use strict'
1
+ import YAML from 'yaml'
2
+
3
+ import { isPlainObject } from './util.js'
2
4
 
3
5
  // parse markdown file:
4
6
  // - extract metadata in yaml format
5
7
  // - extract main title from h1
6
8
 
7
- // todo
9
+ const SEPARATOR = '---'
10
+ const SEPARATOR_END_ALT = '...'
11
+ const LF = '\n'
12
+
13
+ function parse(text) {
14
+ // trim leading LF
15
+ const lines = text.trimStart().split(LF)
16
+
17
+ // console.log(_detectMetaSectionLineQty(lines))
18
+ const metaLineQty = _detectMetaSectionLineQty(lines)
19
+ let meta = {}
20
+ if (metaLineQty > 2) {
21
+ meta = _parseMetaSection(lines, metaLineQty)
22
+ }
23
+
24
+ // 注意:这个函数会修改原数组
25
+ _stripMetaSection(lines, metaLineQty)
26
+
27
+ // 从正文中获取一级标题作为备用 title
28
+ let title = _getTitleFromMainBody(lines[0])
29
+ // 如果采用正文的一级标题作为最终 title,则从正文中去除一级标题,以免复重
30
+ if (!meta.title && title) lines[0] = ''
31
+
32
+ // console.log(lines)
33
+ return {
34
+ meta,
35
+ title,
36
+ content: lines.join(LF).trimStart(),
37
+ }
38
+ }
39
+
40
+ function _detectMetaSectionLineQty(lines) {
41
+ const hasSeparatorStart = lines[0].trimEnd() === SEPARATOR
42
+ const noEmptyLineAfterSeparatorStart = !!lines[1].trimEnd()
43
+ const hasYamlKey = /^#?\s*[\w\-]+:/.test(lines[1].trim())
44
+
45
+ let metaLineQty = 0
46
+ if (hasSeparatorStart && noEmptyLineAfterSeparatorStart && hasYamlKey) {
47
+ // 从第二行 (i = 1) 开始扫描
48
+ for (let i = 1; i < lines.length; i++) {
49
+ let line = lines[i].trimEnd()
50
+ // 遇到结束分隔符,则可以得出行数并退出
51
+ if (line === SEPARATOR || line === SEPARATOR_END_ALT) {
52
+ metaLineQty = i + 1
53
+ break
54
+ }
55
+ // 在遇到结束分隔符之前遇到空行,则视为元数据不存在
56
+ if (!line) break
57
+ }
58
+ }
59
+ return metaLineQty
60
+ }
61
+
62
+ function _parseMetaSection(lines, metaLineQty) {
63
+ // 把分隔符之间的 YAML 代码取出来
64
+ const metaLines = lines.slice(1, metaLineQty - 1)
65
+ const yaml = metaLines.join(LF)
66
+ let result = {}
67
+ try {
68
+ result = YAML.parse(yaml)
69
+ } catch (e) {
70
+ console.error(`[Kup] [Error] Cannot parse YAML meta section:`)
71
+ console.error(SEPARATOR)
72
+ console.error(yaml)
73
+ console.error(SEPARATOR)
74
+ console.error(e.message)
75
+ process.exit(1)
76
+ }
77
+ // console.log(result)
78
+ if (!isPlainObject(result)) {
79
+ console.error(`[Kup] [Error] YAML data must be an Object!`)
80
+ process.exit(1)
81
+ }
82
+
83
+ // format
84
+ const tags = result.tags
85
+ if (!tags) {
86
+ delete result.tags
87
+ } else if (!Array.isArray(tags)) {
88
+ result.tags = [String(tags)]
89
+ }
90
+
91
+ return result
92
+ }
93
+
94
+ // 注意:这个函数会修改原数组
95
+ function _stripMetaSection(lines, metaLineQty) {
96
+ lines.splice(0, metaLineQty)
97
+
98
+ // trim leading empty lines
99
+ let emptyLineQty = 0
100
+ for (let i = 0; i < lines.length; i++) {
101
+ let line = lines[i].trimEnd()
102
+ if (line) {
103
+ break
104
+ } else {
105
+ emptyLineQty++
106
+ }
107
+ }
108
+ if (emptyLineQty) lines.splice(0, emptyLineQty)
109
+ }
110
+
111
+ function _getTitleFromMainBody(firstLine) {
112
+ const re = /^#([^#].*)/
113
+ const result = re.exec(firstLine.trim())
114
+ const title = result ? result[1].trim() : ''
115
+ return title
116
+ }
117
+
118
+ export {
119
+ parse,
120
+ }
package/lib/repo.js ADDED
@@ -0,0 +1,52 @@
1
+ import findUp from 'find-up'
2
+ import path from 'path'
3
+
4
+ import { readTextFile } from './file.js'
5
+ import { validateRepo } from './validate.js'
6
+
7
+ async function getRepo(sourceFile = '') {
8
+ // 目前只实现这一种方式
9
+ return await _getRepoFromPkgKup(sourceFile)
10
+ }
11
+
12
+ async function _getRepoFromPkgKup(sourceFile = '') {
13
+ const json = await _getPkg(sourceFile)
14
+ const repo = json?.kup?.repo
15
+ const result = validateRepo(repo)
16
+ return result.status ? repo : ''
17
+ }
18
+ async function _getRepoFromPkgRepo() {
19
+ // TODO
20
+ }
21
+ async function _getRepoFromGit() {
22
+ // TODO
23
+ }
24
+
25
+ async function _getPkg(sourceFile = '') {
26
+ const cwd = sourceFile ? path.resolve(path.dirname(sourceFile)) : process.cwd()
27
+ const file = await findUp('package.json', { cwd })
28
+
29
+ let pkg = ''
30
+ try {
31
+ pkg = await readTextFile(file)
32
+ } catch (e) {
33
+ console.error(`[Kup] [Error] Cannot read file "${ file }"!`)
34
+ console.error(e.message)
35
+ }
36
+
37
+ let json = null
38
+ try {
39
+ json = JSON.parse(pkg)
40
+ } catch (e) {
41
+ console.error('[Kup] [Error] Cannot parse `package.json` to JSON!')
42
+ console.error(e.message)
43
+ }
44
+ return json
45
+ }
46
+
47
+ export {
48
+ getRepo,
49
+ // __getRepoFromPkgKup: _getRepoFromPkgKup,
50
+ // __getRepoFromPkgRepo: _getRepoFromPkgRepo,
51
+ // __getRepoFromGit: _getRepoFromGit,
52
+ }
package/lib/sync.js ADDED
@@ -0,0 +1,115 @@
1
+ import ghGot from 'gh-got'
2
+ import inquirer from 'inquirer'
3
+
4
+ import { isDebugging } from '../lib/util.js'
5
+
6
+ let proxyOptionsForGot = {}
7
+ if (isDebugging()) {
8
+ const { HttpsProxyAgent } = await import('https-proxy-agent')
9
+ const CHARLES_PROXY = 'http://127.0.0.1:8888'
10
+ const agent = new HttpsProxyAgent(CHARLES_PROXY)
11
+ proxyOptionsForGot = {
12
+ agent: {
13
+ https: agent,
14
+ },
15
+ // 由于 Charles 证书无法通过校验,需要通过这个选项来放行
16
+ rejectUnauthorized: false,
17
+ }
18
+ }
19
+
20
+ async function updateIssue(fileInfo, repo, id) {
21
+ // prepare fields
22
+ // 因为接口是 PATCH 方法,只需要提供需要更新的字段
23
+ // Markdown 文件未提供的字段就不提交了,而不是提交空值,以免清空已有值
24
+ const title = fileInfo.meta.title || fileInfo.title
25
+ const body = fileInfo.content
26
+ const data = { body }
27
+ if (title) data.title = title
28
+ if (fileInfo.meta.tags) data.labels = fileInfo.meta.tags
29
+ if (isDebugging()) {
30
+ console.log('[Kup] [Debug] requestBody =', data)
31
+ }
32
+
33
+ // post
34
+ const api = `/repos/${ repo }/issues/${ id }`
35
+ const method = 'PATCH'
36
+ let response = null
37
+ console.log(`[Kup] Updating "${ repo }#${ id }"...`)
38
+ try {
39
+ response = await ghGot(api, {
40
+ method,
41
+ body: data,
42
+ token: process.env.GITHUB_TOKEN,
43
+ ...proxyOptionsForGot,
44
+ })
45
+ } catch (e) {
46
+ console.error('[Kup] [Error] Request error:', e.message)
47
+ process.exit(1)
48
+ }
49
+
50
+ // handle response
51
+ if (response) {
52
+ const url = `https://github.com/${ repo }/issues/${ id }`
53
+ console.log(`[Kup] [Success] Updated to "${ repo }#${ id }"!`)
54
+ console.log(`[Kup] [Success] URL: ${ url }`)
55
+ }
56
+ }
57
+
58
+ async function postIssue(fileInfo, repo) {
59
+ // prepare fields
60
+ const title = fileInfo.meta.title || fileInfo.title || 'Issue posted by Kup @' + new Date().toISOString()
61
+ const body = fileInfo.content
62
+ const data = { title, body }
63
+ if (fileInfo.meta.tags) data.labels = fileInfo.meta.tags
64
+ if (isDebugging()) {
65
+ console.log('[Kup] [Debug] requestBody =', data)
66
+ }
67
+
68
+ // ask user if Kup should post a new issue
69
+ const answer = await inquirer.prompt([
70
+ {
71
+ name: 'postNewIssue',
72
+ type: 'confirm',
73
+ message: `Kup is going to post a new issue to "${ repo }", OK?`,
74
+ default: true,
75
+ },
76
+ ])
77
+
78
+ // post
79
+ const api = `/repos/${ repo }/issues`
80
+ const method = 'POST'
81
+ let response = null
82
+ if (answer.postNewIssue) {
83
+ console.log(`[Kup] Posting to "${ repo }"...`)
84
+ try {
85
+ response = await ghGot(api, {
86
+ method,
87
+ body: data,
88
+ token: process.env.GITHUB_TOKEN,
89
+ ...proxyOptionsForGot,
90
+ })
91
+ } catch (e) {
92
+ console.error('[Kup] [Error] Request error:', e.message)
93
+ process.exit(1)
94
+ }
95
+ } else {
96
+ console.log('[Kup] Aborted!')
97
+ process.exit(1)
98
+ }
99
+
100
+ // handle response
101
+ if (response) {
102
+ const id = response?.body?.number
103
+ const url = `https://github.com/${ repo }/issues/${ id }`
104
+ console.log(`[Kup] [Success] Posted to "${ repo }#${ id }"!`)
105
+ console.log(`[Kup] [Success] URL: ${ url }`)
106
+
107
+ // write back meta data
108
+ // TODO
109
+ }
110
+ }
111
+
112
+ export {
113
+ updateIssue,
114
+ postIssue,
115
+ }
package/lib/token.js ADDED
@@ -0,0 +1,46 @@
1
+ import inquirer from 'inquirer'
2
+
3
+ async function getToken() {
4
+ let token = process.env.GITHUB_TOKEN
5
+ if (!token) {
6
+ console.error(`[Kup] Cannot find GitHub Token via env variable $GITHUB_TOKEN!`)
7
+ token = await _askToken()
8
+ // 把用户提供的 token 写入环境变量,不过只对当前进程有效
9
+ process.env.GITHUB_TOKEN = token
10
+ } else if (!_validateToken(token)) {
11
+ console.error('[Kup] [Error] The GitHub token you provide is invalid!')
12
+ console.error('[Kup] [Error] The GitHub token must be a "Personal Access Token"!')
13
+ process.exit(1)
14
+ }
15
+ return token
16
+ }
17
+
18
+ async function _askToken() {
19
+ const answer = await inquirer.prompt([
20
+ {
21
+ name: 'token',
22
+ type: 'input',
23
+ message: `Please input your GitHub token here:`,
24
+ },
25
+ ])
26
+ const token = answer.token
27
+ if (!token) {
28
+ console.log('[Kup] Aborted!')
29
+ process.exit(1)
30
+ } else if (!_validateToken(token)) {
31
+ console.error('[Kup] [Error] The GitHub token you provide is invalid!')
32
+ console.error('[Kup] [Error] The GitHub token must be a "Personal Access Token"!')
33
+ process.exit(1)
34
+ }
35
+ return token
36
+ }
37
+
38
+ function _validateToken(token) {
39
+ const RE = /^ghp_\w+$/
40
+ return RE.test(token)
41
+ }
42
+
43
+
44
+ export {
45
+ getToken,
46
+ }
package/lib/util.js ADDED
@@ -0,0 +1,19 @@
1
+ // 考虑到输入源的类型有限,这里只是做了一个简化的实现
2
+ function isPlainObject(obj) {
3
+ if (!obj) return false
4
+ if (typeof obj !== 'object') return false
5
+ if (Array.isArray(obj)) return false
6
+ return true
7
+ }
8
+
9
+
10
+ // run this line in shell to active debug mode:
11
+ // `export KUP_DEBUG_MODE=1`
12
+ function isDebugging() {
13
+ return !!Number(process.env.KUP_DEBUG_MODE)
14
+ }
15
+
16
+ export {
17
+ isPlainObject,
18
+ isDebugging,
19
+ }
@@ -0,0 +1,69 @@
1
+ function validate(argv) {
2
+ const {
3
+ id,
4
+ repo,
5
+ _: files,
6
+ } = argv
7
+
8
+ let results = []
9
+ if ('id' in argv) results.push(validateId(id))
10
+ if ('repo' in argv) results.push(validateRepo(repo))
11
+ results.push(validateFiles(files))
12
+ // 筛选出有错误的结果
13
+ const errorResults = results.filter((result) => result.status === false)
14
+
15
+ // 处理校验结果
16
+ errorResults.forEach((result) => {
17
+ console.error('[Kup] [Error] ' + result.errorMsg)
18
+ })
19
+
20
+ // 目前只能处理第一个文件,因此提示用户
21
+ if (files.length > 1) {
22
+ console.warn('[Kup] [Warning] Limitation: only first file will be handled!')
23
+ }
24
+
25
+ return errorResults.length < 1
26
+ }
27
+
28
+ function validateId(id) {
29
+ let errorMsg = ''
30
+ if (id <= 0 || !Number.isInteger(id)) {
31
+ errorMsg = '`id` must be a positive integer!'
32
+ }
33
+ return {
34
+ status: !errorMsg,
35
+ errorMsg,
36
+ }
37
+ }
38
+ function validateRepo(repo) {
39
+ const REPO_PATTERN = /^[\w\-]+\/[\w\-]+$/
40
+ let errorMsg = ''
41
+ if (!repo) {
42
+ errorMsg = '`repo` must be a string!'
43
+ } else if (!REPO_PATTERN.test(repo)) {
44
+ errorMsg = '`repo` must match `{owner}/{repo}` pattern!'
45
+ }
46
+ return {
47
+ status: !errorMsg,
48
+ errorMsg,
49
+ }
50
+ }
51
+ function validateFiles(files) {
52
+ let errorMsg = ''
53
+ if (!files.length) {
54
+ errorMsg = 'Must specify a local file!'
55
+ } else if (files.some((item) => !item)) {
56
+ errorMsg = 'File name must not be empty!'
57
+ }
58
+ return {
59
+ status: !errorMsg,
60
+ errorMsg,
61
+ }
62
+ }
63
+
64
+ export {
65
+ validate,
66
+ validateId,
67
+ validateRepo,
68
+ validateFiles,
69
+ }
package/package.json CHANGED
@@ -1,15 +1,20 @@
1
1
  {
2
2
  "name": "kup-cli",
3
- "version": "0.0.0-alpha.4",
4
- "description": "A CLI tool to post Markdown files to GitHub as issues.",
3
+ "version": "0.1.0-beta.3",
4
+ "description": "A CLI tool to sync local Markdown files to GitHub issues.",
5
+ "type": "module",
5
6
  "bin": {
6
- "kup": "./bin/cli.js"
7
+ "kup": "bin/cli.js"
7
8
  },
8
9
  "scripts": {
9
- "postpublish": "npm-mirror-sync",
10
+ "debug": "export KUP_DEBUG_MODE=1",
11
+ "postpublish": "npx npm-mirror-sync",
10
12
  "test": "echo \"Error: no test specified\" && exit 1"
11
13
  },
12
- "repository": "cssmagic/kup",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/cssmagic/kup.git"
17
+ },
13
18
  "keywords": [
14
19
  "cli",
15
20
  "markdown",
@@ -19,13 +24,18 @@
19
24
  "author": "cssmagic <cssmagic.cn@gmail.com>",
20
25
  "license": "MIT",
21
26
  "engines": {
22
- "node": ">=12"
27
+ "node": ">=18"
23
28
  },
24
29
  "dependencies": {
30
+ "find-up": "^5.0.0",
25
31
  "gh-got": "^9.0.0",
32
+ "inquirer": "^8.2.4",
26
33
  "yaml": "^2.1.0",
27
34
  "yargs": "^17.5.0"
28
35
  },
36
+ "devDependencies": {
37
+ "https-proxy-agent": "^5.0.1"
38
+ },
29
39
  "files": [
30
40
  "*.js",
31
41
  "lib/",