kup-cli 0.1.0 → 0.1.2

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
@@ -76,8 +76,9 @@ Kup 会把 `file.md` 文件的内容更新到 `foo/bar` 仓库的编号为 `123`
76
76
  1. 调用命令行时指定的 `--repo` 参数。
77
77
  1. Markdown 文件内的 [元数据](https://github.com/cssmagic/kup/issues/1) 的 `repo` 字段。
78
78
  1. 当前项目的 `package.json` 文件内的 `kup.repo` 字段。Kup 会从当前目录向上逐级寻找 `package.json` 文件。
79
+ 1. 如果没有 `kup.repo` 字段,则 Kup 会尝试根据 `repository` 字段来猜测仓库名(在使用前会向用户确认)。
79
80
 
80
- 如果整个项目的同步目标都是同一个仓库,则可以采用最后一种方式统一指定 `repo` 参数。
81
+ 如果整个项目的同步目标都是同一个仓库,则建议采用 `kup.repo` 字段统一指定 `repo` 参数。
81
82
 
82
83
  ### 如何更方便地指定 `id` 参数?
83
84
 
package/bin/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import yargs from 'yargs'
3
3
  import { hideBin } from 'yargs/helpers'
4
+ import { printKupError } from '../lib/error.js'
4
5
  import { isDebugging } from '../lib/util.js'
5
6
  import { main } from '../lib/main.js'
6
7
  import { validate } from '../lib/validate.js'
@@ -48,3 +49,9 @@ main(argv)
48
49
  .then(() => {
49
50
  console.log('[Kup] Done!')
50
51
  })
52
+ .catch((error) => {
53
+ if (!printKupError(error)) {
54
+ console.error(error)
55
+ }
56
+ process.exit(error.exitCode || 1)
57
+ })
package/lib/error.js ADDED
@@ -0,0 +1,39 @@
1
+ class KupError extends Error {
2
+ constructor(outputLines, exitCode = 1) {
3
+ const firstLine = outputLines[0]
4
+ super(firstLine?.text || 'Kup command failed.')
5
+ this.name = 'KupError'
6
+ this.outputLines = outputLines
7
+ this.exitCode = exitCode
8
+ }
9
+ }
10
+
11
+ function errorLine(text) {
12
+ return {
13
+ method: 'error',
14
+ text,
15
+ }
16
+ }
17
+
18
+ function logLine(text) {
19
+ return {
20
+ method: 'log',
21
+ text,
22
+ }
23
+ }
24
+
25
+ function printKupError(error) {
26
+ if (!(error instanceof KupError)) return false
27
+
28
+ error.outputLines.forEach(({ method, text }) => {
29
+ console[method](text)
30
+ })
31
+ return true
32
+ }
33
+
34
+ export {
35
+ KupError,
36
+ errorLine,
37
+ logLine,
38
+ printKupError,
39
+ }
package/lib/file.js CHANGED
@@ -12,7 +12,7 @@ function readTextFile(pathname) {
12
12
  }
13
13
 
14
14
  function writeTextFile(pathname, content) {
15
- fsPromises.writeFile(pathname, content)
15
+ return fsPromises.writeFile(pathname, content)
16
16
  }
17
17
 
18
18
  export {
package/lib/main.js CHANGED
@@ -1,4 +1,7 @@
1
+ import inquirer from 'inquirer'
2
+
1
3
  import { isDebugging } from './util.js'
4
+ import { KupError, errorLine } from './error.js'
2
5
  import { readTextFile } from './file.js'
3
6
  import { parse } from './parse.js'
4
7
  import { getRepo } from './repo.js'
@@ -18,9 +21,10 @@ async function main(argv) {
18
21
  try {
19
22
  content = await readTextFile(file)
20
23
  } catch (e) {
21
- console.error(`[Kup] [Error] Cannot read file "${ file }"!`)
22
- console.error(e.message)
23
- process.exit(1)
24
+ throw new KupError([
25
+ errorLine(`[Kup] [Error] Cannot read file "${ file }"!`),
26
+ errorLine(e.message),
27
+ ])
24
28
  }
25
29
 
26
30
  const fileInfo = parse(content)
@@ -32,12 +36,43 @@ async function main(argv) {
32
36
  }
33
37
 
34
38
  // 通过各种方式获取 repo
35
- // 优先级: 命令行参数 > 文件元数据 > 文件所在项目的 package.json 中的 `kup.repo` 字段
36
- let repoReal = repo || fileInfo.meta.repo
37
- if (!repoReal) repoReal = await getRepo(file)
39
+ // 优先级:
40
+ // P1 命令行参数
41
+ // P2 文件元数据
42
+ // P3 package.json 中的 `kup.repo` 字段
43
+ // P4 package.json 中的 `repository` 字段(需确认)
44
+ let repoReal = ''
45
+ let repoSource = ''
46
+ if (repo) {
47
+ repoReal = repo
48
+ repoSource = 'cli'
49
+ } else if (fileInfo.meta.repo) {
50
+ repoReal = fileInfo.meta.repo
51
+ repoSource = 'meta'
52
+ } else {
53
+ const repoResult = await getRepo(file)
54
+ if (repoResult.needsConfirm) {
55
+ const answer = await inquirer.prompt([
56
+ {
57
+ name: 'useGuessedRepo',
58
+ type: 'confirm',
59
+ message: `Kup guessed the GitHub repo "${ repoResult.repo }" from package.json#repository, use it?`,
60
+ default: true,
61
+ },
62
+ ])
63
+ if (answer.useGuessedRepo) {
64
+ repoReal = repoResult.repo
65
+ repoSource = repoResult.source
66
+ }
67
+ } else if (repoResult.repo) {
68
+ repoReal = repoResult.repo
69
+ repoSource = repoResult.source
70
+ }
71
+ }
38
72
  if (!repoReal) {
39
- console.error('[Kup] [Error] Cannot get `repo` to sync to!')
40
- process.exit(1)
73
+ throw new KupError([
74
+ errorLine('[Kup] [Error] Cannot get `repo` to sync to!'),
75
+ ])
41
76
  }
42
77
 
43
78
  // 检查 TOKEN
@@ -47,7 +82,11 @@ async function main(argv) {
47
82
  if (idReal) {
48
83
  await updateIssue(fileInfo, repoReal, idReal)
49
84
  } else {
50
- await postIssue(fileInfo, repoReal)
85
+ await postIssue(fileInfo, repoReal, {
86
+ file,
87
+ repoSource,
88
+ hasRepoInMeta: !!fileInfo.meta.repo,
89
+ })
51
90
  }
52
91
  }
53
92
  }
package/lib/meta.js ADDED
@@ -0,0 +1,131 @@
1
+ const SEPARATOR = '---'
2
+ const SEPARATOR_END_ALT = '...'
3
+
4
+ function updateIssueMeta(content, {
5
+ id,
6
+ repo = '',
7
+ shouldWriteRepo = false,
8
+ } = {}) {
9
+ const eol = detectEol(content)
10
+ const lines = content.split(eol)
11
+ const metaSection = detectMetaSection(lines)
12
+
13
+ if (!metaSection) {
14
+ return insertNewMetaSection(lines, {
15
+ eol,
16
+ id,
17
+ repo,
18
+ shouldWriteRepo,
19
+ })
20
+ }
21
+
22
+ const metaLines = lines.slice(metaSection.start + 1, metaSection.end)
23
+ upsertMetaLine(metaLines, 'repo', repo, shouldWriteRepo, ['id', 'tags', 'title'])
24
+ upsertMetaLine(metaLines, 'id', String(id), true, ['tags', 'title'], ['repo'])
25
+
26
+ return [
27
+ ...lines.slice(0, metaSection.start + 1),
28
+ ...metaLines,
29
+ ...lines.slice(metaSection.end),
30
+ ].join(eol)
31
+ }
32
+
33
+ function detectEol(content) {
34
+ return content.includes('\r\n') ? '\r\n' : '\n'
35
+ }
36
+
37
+ function detectMetaSection(lines) {
38
+ let start = 0
39
+ while (start < lines.length && !lines[start].trim()) {
40
+ start++
41
+ }
42
+
43
+ const firstLine = lines[start]
44
+ const secondLine = lines[start + 1]
45
+ if (!firstLine || !secondLine) return null
46
+
47
+ const hasSeparatorStart = firstLine.trimEnd() === SEPARATOR
48
+ const noEmptyLineAfterSeparatorStart = !!secondLine.trimEnd()
49
+ const hasYamlKey = /^#?\s*[\w\-]+:/.test(secondLine.trim())
50
+ if (!(hasSeparatorStart && noEmptyLineAfterSeparatorStart && hasYamlKey)) {
51
+ return null
52
+ }
53
+
54
+ for (let i = start + 1; i < lines.length; i++) {
55
+ const line = lines[i].trimEnd()
56
+ if (line === SEPARATOR || line === SEPARATOR_END_ALT) {
57
+ return { start, end: i }
58
+ }
59
+ if (!line) break
60
+ }
61
+
62
+ return null
63
+ }
64
+
65
+ function insertNewMetaSection(lines, { eol, id, repo, shouldWriteRepo }) {
66
+ let insertAt = 0
67
+ while (insertAt < lines.length && !lines[insertAt].trim()) {
68
+ insertAt++
69
+ }
70
+
71
+ const metaLines = [SEPARATOR]
72
+ if (shouldWriteRepo) metaLines.push(`repo: ${ repo }`)
73
+ metaLines.push(`id: ${ id }`)
74
+ metaLines.push(SEPARATOR)
75
+
76
+ const bodyLines = lines.slice(insertAt)
77
+ const result = [...metaLines]
78
+ if (bodyLines.length) {
79
+ result.push('')
80
+ result.push(...bodyLines)
81
+ }
82
+
83
+ return result.join(eol)
84
+ }
85
+
86
+ function upsertMetaLine(metaLines, key, value, shouldWrite, beforeKeys = [], afterKeys = []) {
87
+ if (!shouldWrite) return
88
+
89
+ const existingIndex = findMetaLineIndex(metaLines, key)
90
+ if (existingIndex >= 0) {
91
+ metaLines[existingIndex] = replaceMetaLineValue(metaLines[existingIndex], key, value)
92
+ return
93
+ }
94
+
95
+ const insertIndex = findInsertIndex(metaLines, beforeKeys, afterKeys)
96
+ metaLines.splice(insertIndex, 0, `${ key }: ${ value }`)
97
+ }
98
+
99
+ function findMetaLineIndex(metaLines, key) {
100
+ return metaLines.findIndex((line) => {
101
+ const match = /^(\s*)([\w\-]+)(\s*:.*)$/.exec(line)
102
+ return match?.[2] === key
103
+ })
104
+ }
105
+
106
+ function replaceMetaLineValue(line, key, value) {
107
+ const match = new RegExp(`^(\\s*${ key }\\s*:)(\\s*)(.*?)(\\s+#.*)?$`).exec(line)
108
+ if (!match) return `${ key }: ${ value }`
109
+
110
+ const [, prefix, , , comment = ''] = match
111
+ return `${ prefix } ${ value }${ comment }`
112
+ }
113
+
114
+ function findInsertIndex(metaLines, beforeKeys, afterKeys) {
115
+ for (const key of beforeKeys) {
116
+ const index = findMetaLineIndex(metaLines, key)
117
+ if (index >= 0) return index
118
+ }
119
+
120
+ let afterIndex = -1
121
+ for (const key of afterKeys) {
122
+ const index = findMetaLineIndex(metaLines, key)
123
+ if (index > afterIndex) afterIndex = index
124
+ }
125
+
126
+ return afterIndex >= 0 ? afterIndex + 1 : metaLines.length
127
+ }
128
+
129
+ export {
130
+ updateIssueMeta,
131
+ }
package/lib/parse.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import YAML from 'yaml'
2
2
 
3
+ import { KupError, errorLine } from './error.js'
3
4
  import { isPlainObject } from './util.js'
4
5
 
5
6
  // parse markdown file:
@@ -67,17 +68,19 @@ function _parseMetaSection(lines, metaLineQty) {
67
68
  try {
68
69
  result = YAML.parse(yaml)
69
70
  } 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)
71
+ throw new KupError([
72
+ errorLine('[Kup] [Error] Cannot parse YAML meta section:'),
73
+ errorLine(SEPARATOR),
74
+ errorLine(yaml),
75
+ errorLine(SEPARATOR),
76
+ errorLine(e.message),
77
+ ])
76
78
  }
77
79
  // console.log(result)
78
80
  if (!isPlainObject(result)) {
79
- console.error(`[Kup] [Error] YAML data must be an Object!`)
80
- process.exit(1)
81
+ throw new KupError([
82
+ errorLine('[Kup] [Error] YAML data must be an Object!'),
83
+ ])
81
84
  }
82
85
 
83
86
  // format
package/lib/repo.js CHANGED
@@ -5,18 +5,54 @@ import { readTextFile } from './file.js'
5
5
  import { validateRepo } from './validate.js'
6
6
 
7
7
  async function getRepo(sourceFile = '') {
8
- // 目前只实现这一种方式
9
- return await _getRepoFromPkgKup(sourceFile)
8
+ const json = await _getPkg(sourceFile)
9
+ if (!json) {
10
+ return {
11
+ repo: '',
12
+ source: '',
13
+ needsConfirm: false,
14
+ }
15
+ }
16
+
17
+ const packageRepo = _getRepoFromPkgKup(json)
18
+ if (packageRepo.hasRepoField) {
19
+ return {
20
+ repo: packageRepo.repo,
21
+ source: packageRepo.repo ? 'package' : '',
22
+ needsConfirm: false,
23
+ }
24
+ }
25
+
26
+ const guessedRepo = _getRepoFromPkgRepo(json)
27
+ return {
28
+ repo: guessedRepo,
29
+ source: guessedRepo ? 'package.repository' : '',
30
+ needsConfirm: !!guessedRepo,
31
+ }
10
32
  }
11
33
 
12
- async function _getRepoFromPkgKup(sourceFile = '') {
13
- const json = await _getPkg(sourceFile)
34
+ function _getRepoFromPkgKup(json) {
35
+ const hasRepoField = !!json?.kup && Object.prototype.hasOwnProperty.call(json.kup, 'repo')
14
36
  const repo = json?.kup?.repo
15
37
  const result = validateRepo(repo)
16
- return result.status ? repo : ''
38
+ return {
39
+ repo: result.status ? repo : '',
40
+ hasRepoField,
41
+ }
17
42
  }
18
- async function _getRepoFromPkgRepo() {
19
- // TODO
43
+ function _getRepoFromPkgRepo(json) {
44
+ const repository = json?.repository
45
+ if (!repository) return ''
46
+
47
+ if (typeof repository === 'string') {
48
+ return normalizeRepositoryToRepo(repository)
49
+ }
50
+
51
+ if (typeof repository === 'object' && repository.url) {
52
+ return normalizeRepositoryToRepo(repository.url)
53
+ }
54
+
55
+ return ''
20
56
  }
21
57
  async function _getRepoFromGit() {
22
58
  // TODO
@@ -25,6 +61,7 @@ async function _getRepoFromGit() {
25
61
  async function _getPkg(sourceFile = '') {
26
62
  const cwd = sourceFile ? path.resolve(path.dirname(sourceFile)) : process.cwd()
27
63
  const file = await findUp('package.json', { cwd })
64
+ if (!file) return null
28
65
 
29
66
  let pkg = ''
30
67
  try {
@@ -44,9 +81,36 @@ async function _getPkg(sourceFile = '') {
44
81
  return json
45
82
  }
46
83
 
84
+ function normalizeRepositoryToRepo(repository) {
85
+ if (typeof repository !== 'string') return ''
86
+
87
+ const value = repository.trim()
88
+ if (!value) return ''
89
+
90
+ const candidates = [
91
+ /^github:([\w.-]+\/[\w.-]+)$/i,
92
+ /^(?:git\+)?https?:\/\/github\.com\/([\w.-]+\/[\w.-]+?)(?:\.git)?(?:\/)?$/i,
93
+ /^git@github\.com:([\w.-]+\/[\w.-]+?)(?:\.git)?$/i,
94
+ /^ssh:\/\/git@github\.com\/([\w.-]+\/[\w.-]+?)(?:\.git)?(?:\/)?$/i,
95
+ /^([\w.-]+\/[\w.-]+)$/,
96
+ ]
97
+
98
+ for (const pattern of candidates) {
99
+ const matched = pattern.exec(value)
100
+ if (!matched) continue
101
+
102
+ const repo = matched[1]
103
+ const result = validateRepo(repo)
104
+ if (result.status) return repo
105
+ }
106
+
107
+ return ''
108
+ }
109
+
47
110
  export {
48
111
  getRepo,
49
- // __getRepoFromPkgKup: _getRepoFromPkgKup,
50
- // __getRepoFromPkgRepo: _getRepoFromPkgRepo,
112
+ _getRepoFromPkgKup as __getRepoFromPkgKup,
113
+ _getRepoFromPkgRepo as __getRepoFromPkgRepo,
51
114
  // __getRepoFromGit: _getRepoFromGit,
115
+ normalizeRepositoryToRepo as __normalizeRepositoryToRepo,
52
116
  }
package/lib/sync.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import ghGot from 'gh-got'
2
2
  import inquirer from 'inquirer'
3
3
 
4
+ import { KupError, errorLine, logLine } from './error.js'
5
+ import { readTextFile, writeTextFile } from './file.js'
6
+ import { updateIssueMeta } from './meta.js'
4
7
  import { isDebugging } from '../lib/util.js'
5
8
 
6
9
  let proxyOptionsForGot = {}
@@ -18,14 +21,7 @@ if (isDebugging()) {
18
21
  }
19
22
 
20
23
  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
24
+ const data = buildUpdateIssuePayload(fileInfo)
29
25
  if (isDebugging()) {
30
26
  console.log('[Kup] [Debug] requestBody =', data)
31
27
  }
@@ -43,8 +39,9 @@ async function updateIssue(fileInfo, repo, id) {
43
39
  ...proxyOptionsForGot,
44
40
  })
45
41
  } catch (e) {
46
- console.error('[Kup] [Error] Request error:', e.message)
47
- process.exit(1)
42
+ throw new KupError([
43
+ errorLine('[Kup] [Error] Request error: ' + e.message),
44
+ ])
48
45
  }
49
46
 
50
47
  // handle response
@@ -55,12 +52,8 @@ async function updateIssue(fileInfo, repo, id) {
55
52
  }
56
53
  }
57
54
 
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
55
+ async function postIssue(fileInfo, repo, options = {}) {
56
+ const data = buildPostIssuePayload(fileInfo)
64
57
  if (isDebugging()) {
65
58
  console.log('[Kup] [Debug] requestBody =', data)
66
59
  }
@@ -89,12 +82,14 @@ async function postIssue(fileInfo, repo) {
89
82
  ...proxyOptionsForGot,
90
83
  })
91
84
  } catch (e) {
92
- console.error('[Kup] [Error] Request error:', e.message)
93
- process.exit(1)
85
+ throw new KupError([
86
+ errorLine('[Kup] [Error] Request error: ' + e.message),
87
+ ])
94
88
  }
95
89
  } else {
96
- console.log('[Kup] Aborted!')
97
- process.exit(1)
90
+ throw new KupError([
91
+ logLine('[Kup] Aborted!'),
92
+ ])
98
93
  }
99
94
 
100
95
  // handle response
@@ -104,12 +99,67 @@ async function postIssue(fileInfo, repo) {
104
99
  console.log(`[Kup] [Success] Posted to "${ repo }#${ id }"!`)
105
100
  console.log(`[Kup] [Success] URL: ${ url }`)
106
101
 
107
- // write back meta data
108
- // TODO
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 }`)
109
+ }
110
+ }
111
+ }
112
+
113
+ async function writeIssueMeta(file, { id, repo, shouldWriteRepo }) {
114
+ let content = ''
115
+ try {
116
+ content = await readTextFile(file)
117
+ } catch (e) {
118
+ throw new KupError([
119
+ errorLine(`[Kup] [Error] Cannot read file "${ file }"!`),
120
+ errorLine(e.message),
121
+ ])
122
+ }
123
+
124
+ const nextContent = updateIssueMeta(content, {
125
+ id,
126
+ repo,
127
+ shouldWriteRepo,
128
+ })
129
+
130
+ try {
131
+ await writeTextFile(file, nextContent)
132
+ } catch (e) {
133
+ throw new KupError([
134
+ errorLine(`[Kup] [Error] Cannot write file "${ file }"!`),
135
+ errorLine(e.message),
136
+ ])
109
137
  }
110
138
  }
111
139
 
140
+ function buildUpdateIssuePayload(fileInfo) {
141
+ // 因为接口是 PATCH 方法,只需要提供需要更新的字段
142
+ // Markdown 文件未提供的字段就不提交了,而不是提交空值,以免清空已有值
143
+ const title = fileInfo.meta.title || fileInfo.title
144
+ const body = fileInfo.content
145
+ const data = { body }
146
+ if (title) data.title = title
147
+ if (fileInfo.meta.tags) data.labels = fileInfo.meta.tags
148
+ return data
149
+ }
150
+
151
+ function buildPostIssuePayload(fileInfo) {
152
+ const title = fileInfo.meta.title || fileInfo.title || 'Issue posted by Kup @' + new Date().toISOString()
153
+ const body = fileInfo.content
154
+ const data = { title, body }
155
+ if (fileInfo.meta.tags) data.labels = fileInfo.meta.tags
156
+ return data
157
+ }
158
+
112
159
  export {
160
+ buildPostIssuePayload,
161
+ buildUpdateIssuePayload,
113
162
  updateIssue,
114
163
  postIssue,
164
+ writeIssueMeta,
115
165
  }
package/lib/token.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import inquirer from 'inquirer'
2
2
 
3
+ import { KupError, errorLine, logLine } from './error.js'
4
+
3
5
  async function getToken() {
4
6
  let token = process.env.GITHUB_TOKEN
5
7
  if (!token) {
@@ -7,10 +9,11 @@ async function getToken() {
7
9
  token = await _askToken()
8
10
  // 把用户提供的 token 写入环境变量,不过只对当前进程有效
9
11
  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] It must be a classic (ghp_...) or fine-grained (github_pat_...) PAT.')
13
- process.exit(1)
12
+ } else if (!validateToken(token)) {
13
+ throw new KupError([
14
+ errorLine('[Kup] [Error] The GitHub token you provide is invalid!'),
15
+ errorLine('[Kup] [Error] It must be a classic (ghp_...) or fine-grained (github_pat_...) PAT.'),
16
+ ])
14
17
  }
15
18
  return token
16
19
  }
@@ -25,17 +28,19 @@ async function _askToken() {
25
28
  ])
26
29
  const token = answer.token
27
30
  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] It must be a classic (ghp_...) or fine-grained (github_pat_...) PAT.')
33
- process.exit(1)
31
+ throw new KupError([
32
+ logLine('[Kup] Aborted!'),
33
+ ])
34
+ } else if (!validateToken(token)) {
35
+ throw new KupError([
36
+ errorLine('[Kup] [Error] The GitHub token you provide is invalid!'),
37
+ errorLine('[Kup] [Error] It must be a classic (ghp_...) or fine-grained (github_pat_...) PAT.'),
38
+ ])
34
39
  }
35
40
  return token
36
41
  }
37
42
 
38
- function _validateToken(token) {
43
+ function validateToken(token) {
39
44
  if (typeof token !== 'string' || token.length < 20) return false
40
45
  // classic PAT:ghp_...
41
46
  // fine-grained PAT:github_pat_...
@@ -46,4 +51,5 @@ function _validateToken(token) {
46
51
 
47
52
  export {
48
53
  getToken,
54
+ validateToken,
49
55
  }
package/lib/validate.js CHANGED
@@ -36,7 +36,7 @@ function validateId(id) {
36
36
  }
37
37
  }
38
38
  function validateRepo(repo) {
39
- const REPO_PATTERN = /^[\w\-]+\/[\w\-]+$/
39
+ const REPO_PATTERN = /^[\w.-]+\/[\w.-]+$/
40
40
  let errorMsg = ''
41
41
  if (!repo) {
42
42
  errorMsg = '`repo` must be a string!'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kup-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A CLI tool to sync local Markdown files to GitHub issues.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,10 @@
9
9
  "scripts": {
10
10
  "debug": "export KUP_DEBUG_MODE=1",
11
11
  "postpublish": "npx npm-mirror-sync",
12
- "test": "echo \"Error: no test specified\" && exit 1"
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
14
+ "test:unit": "vitest run tests/unit",
15
+ "test:integration": "vitest run tests/integration"
13
16
  },
14
17
  "repository": {
15
18
  "type": "git",
@@ -34,7 +37,10 @@
34
37
  "yargs": "^18.0.0"
35
38
  },
36
39
  "devDependencies": {
37
- "https-proxy-agent": "^9.0.0"
40
+ "execa": "^9.6.1",
41
+ "https-proxy-agent": "^9.0.0",
42
+ "nock": "^14.0.12",
43
+ "vitest": "^4.1.4"
38
44
  },
39
45
  "files": [
40
46
  "*.js",