@kdcloudjs/cli 0.0.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 +22 -0
- package/package.json +28 -0
- package/src/actions/env/auth/index.js +33 -0
- package/src/actions/env/auth/openapi.js +83 -0
- package/src/actions/env/auth/web.js +3 -0
- package/src/actions/env/config.js +102 -0
- package/src/actions/env/create.js +36 -0
- package/src/actions/env/info.js +28 -0
- package/src/actions/env/list.js +21 -0
- package/src/actions/env/remote.js +59 -0
- package/src/actions/env/remove.js +23 -0
- package/src/actions/env/resolveEnv.js +27 -0
- package/src/actions/env/setDefault.js +20 -0
- package/src/actions/project/constants.js +33 -0
- package/src/actions/project/create/createMetaXml.js +26 -0
- package/src/actions/project/create/index.js +24 -0
- package/src/actions/project/create/kwc.js +25 -0
- package/src/actions/project/create/page.js +42 -0
- package/src/actions/project/create/validate.js +45 -0
- package/src/actions/project/create/writeKwcFiles/index.js +15 -0
- package/src/actions/project/create/writeKwcFiles/lwc.js +25 -0
- package/src/actions/project/create/writeKwcFiles/react.js +49 -0
- package/src/actions/project/create/writeKwcFiles/vue.js +48 -0
- package/src/actions/project/deploy/collect.js +43 -0
- package/src/actions/project/deploy/context.js +26 -0
- package/src/actions/project/deploy/index.js +38 -0
- package/src/actions/project/deploy/projectRoot.js +32 -0
- package/src/actions/project/deploy/updateIsv.js +109 -0
- package/src/actions/project/deploy/upload.js +60 -0
- package/src/actions/project/deploy/validate.js +13 -0
- package/src/actions/project/init/index.js +42 -0
- package/src/actions/project/init/post.js +20 -0
- package/src/actions/project/init/prompts.js +50 -0
- package/src/api/index.js +99 -0
- package/src/api/uploader/index.js +7 -0
- package/src/api/uploader/updateKwcMeta.js +90 -0
- package/src/api/uploader/updatePageMeta.js +94 -0
- package/src/commands/env/auth.js +8 -0
- package/src/commands/env/create.js +2 -0
- package/src/commands/env/delete.js +2 -0
- package/src/commands/env/index.js +72 -0
- package/src/commands/env/info.js +2 -0
- package/src/commands/env/list.js +2 -0
- package/src/commands/env/remote.js +2 -0
- package/src/commands/env/set.js +2 -0
- package/src/commands/project/create.js +8 -0
- package/src/commands/project/deploy.js +2 -0
- package/src/commands/project/index.js +28 -0
- package/src/commands/project/init.js +5 -0
- package/src/index.js +24 -0
- package/src/utils/checkUpdate.js +18 -0
- package/src/utils/crypto.js +53 -0
- package/src/utils/download.js +21 -0
- package/src/utils/index.js +9 -0
- package/src/utils/log.js +20 -0
- package/src/utils/printTable.js +28 -0
- package/src/utils/projectConfig.js +32 -0
- package/src/utils/prompts.js +14 -0
- package/src/utils/validator.js +14 -0
package/src/api/index.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const { error } = require('../utils/log')
|
|
2
|
+
const crypto = require('crypto')
|
|
3
|
+
async function getAllDataCenters (url) {
|
|
4
|
+
if (!url) return []
|
|
5
|
+
const res = []
|
|
6
|
+
try {
|
|
7
|
+
url = url.trim().replace(/\/+$/, '')
|
|
8
|
+
const resp = await fetch(`${url}/auth/getAllDatacenters.do`)
|
|
9
|
+
if (resp?.status === 200) {
|
|
10
|
+
const data = await resp.json()
|
|
11
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
12
|
+
for (let i = 0; i < data.length; i++) {
|
|
13
|
+
const d = data[i]
|
|
14
|
+
if (d.accountId) {
|
|
15
|
+
res.push({
|
|
16
|
+
...d,
|
|
17
|
+
title: d.accountName,
|
|
18
|
+
value: d.accountId
|
|
19
|
+
})
|
|
20
|
+
} else {
|
|
21
|
+
error(`⚠ 数据中心返回异常,请检查url: ${url} 是否正确`)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
error(`⚠ 数据中心列表为空,请检查url: ${url} 是否正确`)
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
error(`⚠ 请求失败,请检查url: ${url} 是否正确。error message: ${resp?.statusText}`)
|
|
29
|
+
}
|
|
30
|
+
} catch (e) {
|
|
31
|
+
error(`⚠ 请求失败,请检查url: ${url} 是否正确。error message: ${e?.message}`)
|
|
32
|
+
}
|
|
33
|
+
return res
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function getAccessToken (url, params) {
|
|
37
|
+
if (!url) return {}
|
|
38
|
+
let res = {}
|
|
39
|
+
try {
|
|
40
|
+
// 构造请求体
|
|
41
|
+
const body = {
|
|
42
|
+
client_id: params.client_id,
|
|
43
|
+
client_secret: params.client_secret,
|
|
44
|
+
username: params.username,
|
|
45
|
+
accountId: params.accountId,
|
|
46
|
+
language: 'zh_CN',
|
|
47
|
+
nonce: crypto.randomBytes(8).toString('hex'),
|
|
48
|
+
timestamp: `${Date.now()}`
|
|
49
|
+
}
|
|
50
|
+
url = url.trim().replace(/\/+$/, '')
|
|
51
|
+
const resp = await fetch(`${url}/kapi/oauth2/getToken`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
body: JSON.stringify(body)
|
|
55
|
+
})
|
|
56
|
+
if (resp?.status === 200) {
|
|
57
|
+
const respData = await resp.json()
|
|
58
|
+
if (respData?.status) {
|
|
59
|
+
res = respData?.data || {}
|
|
60
|
+
} else {
|
|
61
|
+
error(`⚠ 获取access_token失败,请检查url、client_id、client_secret、username是否正确。errormessage: ${respData?.message}`)
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
error(`⚠ 获取access_token失败,请检查url、client_id、client_secret、username是否正确, response status: ${resp?.status}`)
|
|
65
|
+
}
|
|
66
|
+
} catch (e) {
|
|
67
|
+
error(`⚠ 获取access_token失败,请检查url、client_id、client_secret、username是否正确。error message: ${e?.message}`)
|
|
68
|
+
}
|
|
69
|
+
return res
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function getIsv (url, access_token) {
|
|
73
|
+
if (!url || !access_token) return {}
|
|
74
|
+
let res = {}
|
|
75
|
+
const baseUrl = url.trim().replace(/\/+$/, '')
|
|
76
|
+
const isvUrl = `${baseUrl}/kapi/v2/mdl/kwcisv`
|
|
77
|
+
try {
|
|
78
|
+
const resp = await fetch(isvUrl, {
|
|
79
|
+
method: 'GET',
|
|
80
|
+
headers: { 'Content-Type': 'application/json', access_token }
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
if (resp.status === 200) {
|
|
84
|
+
const respData = await resp.json()
|
|
85
|
+
if (respData?.status) {
|
|
86
|
+
res = respData?.data || {}
|
|
87
|
+
} else {
|
|
88
|
+
error(`⚠ get isv fail,pls check url and credentials info, errormessage: ${respData?.message}`)
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
error(`⚠ get isv fail,pls check url and credentials info, response status: ${resp?.status}`)
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
error(`⚠ getIsv fail。error message: ${err.message}`)
|
|
95
|
+
}
|
|
96
|
+
return res
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { getAllDataCenters, getAccessToken, getIsv }
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const FormData = require('form-data')
|
|
4
|
+
const axios = require('axios')
|
|
5
|
+
const { getAccessToken } = require('../../api')
|
|
6
|
+
const { isValidUrl } = require('../../utils/validator')
|
|
7
|
+
const { decrypt } = require('../../utils/crypto')
|
|
8
|
+
|
|
9
|
+
module.exports.updateKwcMeta = async function updateKwcMeta(filePath, env = {}) {
|
|
10
|
+
// TODO: 调用真实接口
|
|
11
|
+
if (!fs.existsSync(filePath)) {
|
|
12
|
+
throw new Error(`❌ 未找到文件: ${filePath}`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { url: backendUrl, client_id, client_secret, username, accountId, access_token } = env
|
|
16
|
+
|
|
17
|
+
// 这里accessToken可能已过期,所以如果有client_id、client_secret、username、accountId这些信息就可以重新获取一下token
|
|
18
|
+
// todo,现在先写死
|
|
19
|
+
// 获取token
|
|
20
|
+
if (!isValidUrl(backendUrl)) {
|
|
21
|
+
throw new Error('Invalid URL format:', backendUrl)
|
|
22
|
+
}
|
|
23
|
+
let token = access_token
|
|
24
|
+
if (client_id && client_secret && username && accountId) {
|
|
25
|
+
const getAccessTokenParams = {
|
|
26
|
+
client_id,
|
|
27
|
+
client_secret: decrypt(client_secret),
|
|
28
|
+
username,
|
|
29
|
+
accountId
|
|
30
|
+
}
|
|
31
|
+
const tokenData = await getAccessToken(backendUrl, getAccessTokenParams) || {}
|
|
32
|
+
token = tokenData.access_token || ''
|
|
33
|
+
if (!token) {
|
|
34
|
+
throw new Error('Failed to obtain access_token. Please check your credentials and reauthentication.')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 去掉 backendUrl 尾部的 /
|
|
39
|
+
const baseUrl = backendUrl.trim().replace(/\/+$/, '')
|
|
40
|
+
const url = `${baseUrl}/kapi/v2/mdl/updateKwc`
|
|
41
|
+
|
|
42
|
+
// ============ 第一阶段:文本方式上传 ============
|
|
43
|
+
try {
|
|
44
|
+
const xmlContent = fs.readFileSync(filePath, 'utf-8')
|
|
45
|
+
|
|
46
|
+
const res = await axios.post(
|
|
47
|
+
url,
|
|
48
|
+
{
|
|
49
|
+
content: xmlContent, // ✅ 将 XML 文件内容作为纯文本传输
|
|
50
|
+
filename: path.basename(filePath), // 文件名
|
|
51
|
+
forceSave: 'true'
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
access_token: token
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if (res.status === 200 && res.data?.status) {
|
|
62
|
+
return res
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.warn(`⚠ 文本上传失败,尝试文件上传方式: ${err.message}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============ 第二阶段:FormData 文件流上传 ============
|
|
69
|
+
try {
|
|
70
|
+
// 构造 FormData
|
|
71
|
+
const formData = new FormData()
|
|
72
|
+
formData.append('file', fs.createReadStream(filePath), {
|
|
73
|
+
filename: path.basename(filePath)
|
|
74
|
+
})
|
|
75
|
+
formData.append('forceSave', 'true')
|
|
76
|
+
|
|
77
|
+
const res = await axios.post(url, formData, {
|
|
78
|
+
headers: {
|
|
79
|
+
access_token: token, // ✅ 后端要求的 header
|
|
80
|
+
...formData.getHeaders() // ✅ 自动补上 multipart 边界
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
if (res.status === 200 && res.data?.status) {
|
|
84
|
+
return res
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`File upload failed: ${res?.data?.message || res.status}`)
|
|
87
|
+
} catch (err) {
|
|
88
|
+
throw new Error(`元数据上传失败:${err.message}`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// const { getAccessToken } = require('../../api')
|
|
2
|
+
// module.exports.updatePageMeta = async function updatePageMeta(files, env) {
|
|
3
|
+
// // TODO: 调用真实接口
|
|
4
|
+
// // console.log(files, env, 'updatePageMeta')
|
|
5
|
+
// }
|
|
6
|
+
const fs = require('fs')
|
|
7
|
+
const path = require('path')
|
|
8
|
+
const FormData = require('form-data')
|
|
9
|
+
const axios = require('axios')
|
|
10
|
+
const { getAccessToken } = require('../../api')
|
|
11
|
+
const { isValidUrl } = require('../../utils/validator')
|
|
12
|
+
const { decrypt } = require('../../utils/crypto')
|
|
13
|
+
|
|
14
|
+
module.exports.updatePageMeta = async function updatePageMeta(filePath, env = {}) {
|
|
15
|
+
// TODO: 调用真实接口
|
|
16
|
+
if (!fs.existsSync(filePath)) {
|
|
17
|
+
throw new Error(`❌ 未找到文件: ${filePath}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { url: backendUrl, client_id, client_secret, username, accountId, access_token } = env
|
|
21
|
+
|
|
22
|
+
// 这里accessToken可能已过期,所以如果有client_id、client_secret、username、accountId这些信息就可以重新获取一下token
|
|
23
|
+
// todo,现在先写死
|
|
24
|
+
// 获取token
|
|
25
|
+
if (!isValidUrl(backendUrl)) {
|
|
26
|
+
throw new Error('Invalid URL format:', backendUrl)
|
|
27
|
+
}
|
|
28
|
+
let token = access_token
|
|
29
|
+
if (client_id && client_secret && username && accountId) {
|
|
30
|
+
const getAccessTokenParams = {
|
|
31
|
+
client_id,
|
|
32
|
+
client_secret: decrypt(client_secret),
|
|
33
|
+
username,
|
|
34
|
+
accountId
|
|
35
|
+
}
|
|
36
|
+
const tokenData = await getAccessToken(backendUrl, getAccessTokenParams) || {}
|
|
37
|
+
token = tokenData.access_token || ''
|
|
38
|
+
if (!token) {
|
|
39
|
+
throw new Error('Failed to obtain access_token. Please check your credentials and reauthentication.')
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 去掉 backendUrl 尾部的 /
|
|
44
|
+
const baseUrl = backendUrl.trim().replace(/\/+$/, '')
|
|
45
|
+
const url = `${baseUrl}/kapi/v2/mdl/updatePageMeta`
|
|
46
|
+
|
|
47
|
+
// ============ 第一阶段:文本方式上传 ============
|
|
48
|
+
try {
|
|
49
|
+
const xmlContent = fs.readFileSync(filePath, 'utf-8')
|
|
50
|
+
|
|
51
|
+
const res = await axios.post(
|
|
52
|
+
url,
|
|
53
|
+
{
|
|
54
|
+
content: xmlContent, // ✅ 将 XML 文件内容作为纯文本传输
|
|
55
|
+
filename: path.basename(filePath), // 文件名
|
|
56
|
+
forceSave: 'true'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
access_token: token
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
if (res.status === 200 && res.data?.status) {
|
|
66
|
+
return res
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.warn(`⚠ 文本上传失败,尝试文件上传方式: ${err.message}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============ 第二阶段:FormData 文件流上传 ============
|
|
73
|
+
try {
|
|
74
|
+
// 构造 FormData
|
|
75
|
+
const formData = new FormData()
|
|
76
|
+
formData.append('file', fs.createReadStream(filePath), {
|
|
77
|
+
filename: path.basename(filePath)
|
|
78
|
+
})
|
|
79
|
+
formData.append('forceSave', 'true')
|
|
80
|
+
|
|
81
|
+
const res = await axios.post(url, formData, {
|
|
82
|
+
headers: {
|
|
83
|
+
access_token: token, // ✅ 后端要求的 header
|
|
84
|
+
...formData.getHeaders() // ✅ 自动补上 multipart 边界
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
if (res.status === 200 && res.data?.status) {
|
|
88
|
+
return res
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`File upload failed: ${res?.data?.message || res.status}`)
|
|
91
|
+
} catch (err) {
|
|
92
|
+
throw new Error(`元数据上传失败:${err.message}`)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module.exports = function registerEnv(program) {
|
|
2
|
+
const env = program
|
|
3
|
+
.command('env')
|
|
4
|
+
.description('Environment management')
|
|
5
|
+
|
|
6
|
+
env
|
|
7
|
+
.command('create <name>')
|
|
8
|
+
.description('Create a new environment')
|
|
9
|
+
.option('--url <url>', 'environment url')
|
|
10
|
+
.action((name, options) =>
|
|
11
|
+
require('./create')(name, options)
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
env
|
|
15
|
+
.command('set')
|
|
16
|
+
.description('Set environment config')
|
|
17
|
+
.command('target-env <name>')
|
|
18
|
+
.description('Set default environment')
|
|
19
|
+
.action(name =>
|
|
20
|
+
require('./set')(name)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
env
|
|
24
|
+
.command('info')
|
|
25
|
+
.description('Show current environment info')
|
|
26
|
+
.action(() =>
|
|
27
|
+
require('./info')()
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
env
|
|
31
|
+
.command('list')
|
|
32
|
+
.description('List all environments')
|
|
33
|
+
.action(() =>
|
|
34
|
+
require('./list')()
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
env
|
|
38
|
+
.command('delete <name>')
|
|
39
|
+
.description('Delete an environment')
|
|
40
|
+
.action(name =>
|
|
41
|
+
require('./delete')(name)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
env
|
|
45
|
+
.command('remote [envName]')
|
|
46
|
+
.description('Open remote environment page with local debug server')
|
|
47
|
+
.requiredOption('-f, --form-id <id>', 'Form ID')
|
|
48
|
+
.action((envName, options) =>
|
|
49
|
+
require('./remote')(envName, options)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const auth = env
|
|
53
|
+
.command('auth')
|
|
54
|
+
.description('Authenticate environment')
|
|
55
|
+
|
|
56
|
+
auth
|
|
57
|
+
.command('web')
|
|
58
|
+
.description('Authenticate via Web (username/password)')
|
|
59
|
+
.option('--env <name>', 'target environment name')
|
|
60
|
+
.action((options) =>
|
|
61
|
+
require('./auth')('web', options)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
auth
|
|
65
|
+
.command('openapi')
|
|
66
|
+
.description('Authenticate via OpenAPI (client credentials)')
|
|
67
|
+
.option('--env <name>', 'target environment name')
|
|
68
|
+
.action((options) =>
|
|
69
|
+
require('./auth')('openapi', options)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module.exports = function registerProject(program) {
|
|
2
|
+
const project = program
|
|
3
|
+
.command('project')
|
|
4
|
+
.description('Project management')
|
|
5
|
+
|
|
6
|
+
project
|
|
7
|
+
.command('init <name>')
|
|
8
|
+
.description('Initialize a new project')
|
|
9
|
+
.option('-s, --source <source>', 'specified repository source', 'outer')
|
|
10
|
+
.action((name, options) => require('./init')(name, options))
|
|
11
|
+
|
|
12
|
+
project
|
|
13
|
+
.command('create <name>')
|
|
14
|
+
.description('Create control or page xml')
|
|
15
|
+
.option('--type <type>', 'project type: kwc | page')
|
|
16
|
+
.action((name, options) =>
|
|
17
|
+
require('./create')(name, options)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
project
|
|
21
|
+
.command('deploy')
|
|
22
|
+
.description('Deploy current project')
|
|
23
|
+
.option('-e --target-env <env>', 'specified target environment')
|
|
24
|
+
.option('-d --source-dir <dir>', 'specified source directory to deploy')
|
|
25
|
+
// .option('-f --force', 'force deploy, ignore warnings')
|
|
26
|
+
// .option('-a --all', 'deploy all metadata')
|
|
27
|
+
.action((options) => require('./deploy')(options))
|
|
28
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander')
|
|
4
|
+
const pkg = require('../package.json')
|
|
5
|
+
const checkUpdate = require('./utils/checkUpdate')
|
|
6
|
+
|
|
7
|
+
const program = new Command()
|
|
8
|
+
|
|
9
|
+
checkUpdate()
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('kd')
|
|
13
|
+
.description('Kingdee CLI')
|
|
14
|
+
.version(pkg.version)
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command('update')
|
|
18
|
+
.description('update kd to latest version')
|
|
19
|
+
.action(() => require('./commands/update')())
|
|
20
|
+
|
|
21
|
+
require('./commands/project')(program)
|
|
22
|
+
require('./commands/env')(program)
|
|
23
|
+
|
|
24
|
+
program.parse(process.argv)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const pkg = require('../../package.json')
|
|
2
|
+
|
|
3
|
+
async function checkUpdate() {
|
|
4
|
+
const { default: updateNotifier } = await import('update-notifier')
|
|
5
|
+
const notifier = updateNotifier({
|
|
6
|
+
pkg,
|
|
7
|
+
updateCheckInterval: 1000 * 60 * 60 * 24 // 1 天
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
if (notifier.update) {
|
|
11
|
+
notifier.notify({
|
|
12
|
+
defer: false,
|
|
13
|
+
isGlobal: true
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = checkUpdate
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const crypto = require('crypto')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const os = require('os')
|
|
5
|
+
|
|
6
|
+
const KEY_FILE = path.join(os.homedir(), '.kd', 'secret.key')
|
|
7
|
+
const ALGORITHM = 'aes-256-cbc'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 获取加密密钥,如果不存在则自动生成并保存
|
|
11
|
+
*/
|
|
12
|
+
function getKey () {
|
|
13
|
+
if (!fs.existsSync(KEY_FILE)) {
|
|
14
|
+
const dir = path.dirname(KEY_FILE)
|
|
15
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
16
|
+
const key = crypto.randomBytes(32) // 256 bit
|
|
17
|
+
fs.writeFileSync(KEY_FILE, key.toString('hex'), 'utf-8')
|
|
18
|
+
return key
|
|
19
|
+
}
|
|
20
|
+
return Buffer.from(fs.readFileSync(KEY_FILE, 'utf-8'), 'hex')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 加密文本
|
|
25
|
+
* @param {string} text - 明文
|
|
26
|
+
* @returns {string} - base64编码的密文(包含 iv)
|
|
27
|
+
*/
|
|
28
|
+
function encrypt (text) {
|
|
29
|
+
const key = getKey()
|
|
30
|
+
const iv = crypto.randomBytes(16) // 128 bit
|
|
31
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
|
|
32
|
+
let encrypted = cipher.update(text, 'utf8', 'base64')
|
|
33
|
+
encrypted += cipher.final('base64')
|
|
34
|
+
return iv.toString('base64') + ':' + encrypted
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 解密文本
|
|
39
|
+
* @param {string} data - base64编码的密文(iv:密文)
|
|
40
|
+
* @returns {string} - 明文
|
|
41
|
+
*/
|
|
42
|
+
function decrypt (data) {
|
|
43
|
+
if (!data) return ''
|
|
44
|
+
const key = getKey()
|
|
45
|
+
const [ivStr, encrypted] = data.split(':')
|
|
46
|
+
const iv = Buffer.from(ivStr, 'base64')
|
|
47
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
|
|
48
|
+
let decrypted = decipher.update(encrypted, 'base64', 'utf8')
|
|
49
|
+
decrypted += decipher.final('utf8')
|
|
50
|
+
return decrypted
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { encrypt, decrypt }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const download = require('download-git-repo')
|
|
2
|
+
const ora = require('ora')
|
|
3
|
+
const { error, success } = require('./log.js')
|
|
4
|
+
|
|
5
|
+
module.exports = function downloadTemplate(repo, dest, template) {
|
|
6
|
+
const spinner = ora('downloading ...').start()
|
|
7
|
+
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
download(repo, dest, { clone: true }, err => {
|
|
10
|
+
if (err) {
|
|
11
|
+
spinner.fail()
|
|
12
|
+
error(`Template ${template} download failed: ${err.message || err}`)
|
|
13
|
+
reject(err)
|
|
14
|
+
} else {
|
|
15
|
+
spinner.stop()
|
|
16
|
+
success('\n✅ project created successfully!\n')
|
|
17
|
+
resolve()
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
exports.capitalize = function (name) {
|
|
2
|
+
return name[0].toUpperCase() + name.slice(1)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
exports.toPascalCase = function (name) {
|
|
6
|
+
return name
|
|
7
|
+
.replace(/(-\w)/g, (match) => match[1].toUpperCase()) // kebab-case to camelCase
|
|
8
|
+
.replace(/^[a-z]/, (match) => match.toUpperCase()) // first char to Upper
|
|
9
|
+
}
|
package/src/utils/log.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const chalk = require('chalk')
|
|
2
|
+
|
|
3
|
+
const warn = (msg) => console.log(chalk.yellow(msg))
|
|
4
|
+
const error = (msg) => console.log(chalk.red(msg))
|
|
5
|
+
|
|
6
|
+
const success = (msg) => console.log(chalk.green(msg))
|
|
7
|
+
|
|
8
|
+
const mark = (msg) => console.log(chalk.magenta(msg))
|
|
9
|
+
|
|
10
|
+
const info = (msg) => console.log(chalk.gray(msg))
|
|
11
|
+
const tip = (msg) => console.log(chalk.cyan(msg))
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
warn,
|
|
15
|
+
error,
|
|
16
|
+
success,
|
|
17
|
+
mark,
|
|
18
|
+
info,
|
|
19
|
+
tip
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function pad(str, len) {
|
|
2
|
+
str = String(str)
|
|
3
|
+
return str + ' '.repeat(Math.max(len - str.length, 0))
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
module.exports = function printTable(headers, rows) {
|
|
7
|
+
const colWidths = headers.map((h, i) =>
|
|
8
|
+
Math.max(
|
|
9
|
+
h.length,
|
|
10
|
+
...rows.map(r => String(r[i] ?? '').length)
|
|
11
|
+
)
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const line = headers
|
|
15
|
+
.map((h, i) => pad(h, colWidths[i]))
|
|
16
|
+
.join(' ')
|
|
17
|
+
|
|
18
|
+
console.log(line)
|
|
19
|
+
console.log(
|
|
20
|
+
colWidths.map(w => '-'.repeat(w)).join(' ')
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
rows.forEach(row => {
|
|
24
|
+
console.log(
|
|
25
|
+
row.map((cell, i) => pad(cell ?? '', colWidths[i])).join(' ')
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
|
|
4
|
+
function getConfig(root) {
|
|
5
|
+
const configPath = path.join(root, '.kd', 'config.json')
|
|
6
|
+
|
|
7
|
+
if (!fs.existsSync(configPath)) {
|
|
8
|
+
throw new Error('未找到 .kd/config.json')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getConfigValue(root, key) {
|
|
15
|
+
const config = getConfig(root)
|
|
16
|
+
|
|
17
|
+
if (!config[key]) {
|
|
18
|
+
throw new Error(`config.json 中缺少 ${key}`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return config[key]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
exports.getConfig = getConfig
|
|
25
|
+
|
|
26
|
+
exports.getModuleId = function (root) {
|
|
27
|
+
return getConfigValue(root, 'moduleId')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
exports.getFramework = function (root) {
|
|
31
|
+
return getConfigValue(root, 'framework')
|
|
32
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const prompts = require('prompts')
|
|
2
|
+
|
|
3
|
+
function safePrompts (questions) {
|
|
4
|
+
return prompts(questions, {
|
|
5
|
+
onCancel: () => {
|
|
6
|
+
console.log('\n❌ action already canceled, exiting...')
|
|
7
|
+
process.exit(0)
|
|
8
|
+
}
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
safePrompts
|
|
14
|
+
}
|